Skip to content

refactor: revamp bitcoindeepa tma#64

Open
helloscoopa wants to merge 2 commits into
mainfrom
centi/revamp-bitcoindeepa-tma
Open

refactor: revamp bitcoindeepa tma#64
helloscoopa wants to merge 2 commits into
mainfrom
centi/revamp-bitcoindeepa-tma

Conversation

@helloscoopa

@helloscoopa helloscoopa commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features

    • Redesigned dashboard with wallet balance display, DCA summary, and active plan information
    • New onboarding flow with user progress tracking and tier-based progression
    • Enhanced subscription management with improved plan selection and membership handling
    • Expanded bottom navigation with additional sections for history, tasks, and news
    • Added multiple new UI components for consistent design across the app
  • Refactor

    • Overhauled dashboard layout with Telegram-based authentication and KYC verification
    • Restructured home page experience with distinct flows for new and returning users
    • Modernized subscription page with better plan organization and easier management
  • Style

    • Updated loading page branding and visual styling
    • Refined navigation bar with improved pill-shaped layout and better accessibility
    • Enhanced button and card components with improved visual feedback

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bitcoin-deepa-tma-bot Error Error Jun 12, 2026 4:00pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive UI component library and rebuilds the home page and dashboard surfaces. The new src/components/ui/* components provide form, display, and toggle controls used across pages. The home page now implements an onboarding flow with user-progress tier tracking and conditional screen rendering for new vs. existing users. The dashboard wallet page fetches DCA summary and subscription data, enforces KYC approval, and renders a card-based interface with balance-masking and rewards tracking. The subscription page restructures around tab-based plan selection and active membership management with PayHere integration.

Changes

Shared UI Component Library

Layer / File(s) Summary
Form & Input Components
src/components/ui/button.tsx, src/components/ui/text-field.tsx, src/components/ui/search-field.tsx, src/components/ui/copy-field.tsx
Button exports variant/size types and supports loading/disabled states with optional left icon. TextField and TextAreaField are forward-ref form controls with state-driven styling and error display. SearchField is a ref-forwarded input with default placeholder. CopyField manages clipboard write with temporary "Copied!" feedback.
Display & Selection Components
src/components/ui/page-title.tsx, src/components/ui/plan-card.tsx, src/components/ui/clickable-card.tsx
PageTitle renders styled title and optional subtitle. PlanCard shows plan emoji/name/price/description with "Most Popular" badge and select state. ClickableCard renders icon/title/subtitle with chevron and disabled state detection.
Toggle & Filter Components
src/components/ui/toggle-plan.tsx, src/components/ui/category-filter.tsx, src/components/ui/visible-toggle.tsx
TogglePlan renders weekly/monthly toggle buttons. CategoryFilter and CategoryFilterGroup provide clickable filter chips with active-state styling. VisibleToggle renders eye-open/closed icon toggle with optional dark variant.

Home Page & Navigation Shell

Layer / File(s) Summary
Home Page Onboarding Flow
src/app/page.tsx
Replaces simple landing redirect with multi-screen entry: UserProgress component fetches /api/user count and renders tier-based progress bar; PageShell provides background and centered layout; NewUserScreen and ExistingUserScreen render conditionally based on getAuthTokenFromStorage() or store isExistingUser, wiring onStart/onOpen callbacks and Telegram link navigation for plans/gifting.
Navigation & Loading UI
src/components/bottomNavigation.tsx, src/components/LoadingPage.tsx
BottomNavigation switches from external icon libraries to five inline SVG icons, updates fixed positioning with z-50 and pointer-events-none at container level, and maps 5 routes (/dashboard, /dashboard/history, /dashboard/subscription, /task, /news). LoadingPage removes animated-dot state management and simplifies to static spinner/logo display with BDLogo_Black.svg asset and updated color classes.

Dashboard Pages

Layer / File(s) Summary
Wallet Dashboard Page
src/app/dashboard/page.tsx
Rewrites dashboard as WalletPage: initializes Telegram auth via useLaunchParams, stores auth token, checks user registration via /api/user (redirects to /onboard if not registered), fetches DCA summary and current subscription via authorized useQuery, performs KYC status check (/api/user/kyc/status) with redirect to /verification if not APPROVED. Renders card-based layout with VisibleToggle for masked-balance display, PlanCard for subscription details with next/last reward dates, InvestmentCard computing profit/loss and percentage, RewardsChart for performance visualization, and BottomNavigation.
Subscription Management Page
src/app/dashboard/subscription/page.tsx
Restructures page around tab-based UI (Plans vs My Membership): fetches packages via React Query, derives current subscription from /api/subscription/current, auto-selects first plan matching selected duration. TabBar component switches tabs; ActiveMembershipCard displays active subscription with destructive cancel button and inline error text; NoMembership renders empty state with "View Plans" CTA. Subscribe handler generates PayHere redirect via /api/subscription/payhere-link (POST with bearer auth); cancel handler calls /api/subscription/cancel and clears subscription state. Plans tab shows duration toggle, plan cards, and subscribe button with "Already Subscribed" message when active membership exists.

Dev & Configuration

Layer / File(s) Summary
Dev Page & Port Configuration
src/app/dev/page.tsx, package.json
New DevPage previews all UI components with local state for plan selection, duration, category, and text input. Development server port changed from 3347 to 3348.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

do not merge, Review effort 4/5, enhancement

Suggested reviewers

  • mrcentimetre
  • rayaanr

Poem

🐰 A rabbit's ode to the refactor

Component cards hop in to stay,
New pages dance the forms at play,
Telegram auth guards the way,
With plans and subscriptions on display,
The dashboard blooms a brighter day! 🌱✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title uses the abbreviation 'tma' which is not immediately clear; 'bitcoindeepa' appears to be a product/project name. The term 'revamp' is somewhat vague regarding specific changes. Consider using a more descriptive title that explains the primary change, such as 'refactor: redesign dashboard and subscription UI with new components' or 'refactor: revamp TMA dashboard with new wallet UI and components'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch centi/revamp-bitcoindeepa-tma
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch centi/revamp-bitcoindeepa-tma

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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.

@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: 15

🤖 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/dashboard/page.tsx`:
- Around line 312-319: The KYC check currently only redirects when the fetch
succeeds and returns data.status !== "APPROVED", allowing failures (non-OK
responses or errors) to bypass the gate; update the useEffect fetch to treat any
non-ok response or thrown error as untrusted and redirect to
router.push("/verification") — i.e., inspect the response (from
fetch("/api/user/kyc/status") with authToken) and if !r.ok or data is missing or
data.status !== "APPROVED", call router.push("/verification"), and also call
router.push("/verification") in the catch handler to fail closed.
- Around line 263-270: The query for useQuery (queryKey "subscription-current")
is returning the API shape { subscription, message } not the UI Subscription
expected by PlanCard, so normalize the response before passing it down: in the
queryFn that fetches "/api/subscription/current" (used by useQuery) extract the
inner subscription object, map/transform its fields to the Subscription UI shape
PlanCard expects (ensure properties like planName, price, startDate, endDate are
present or set to null/defaults), and return that normalized Subscription | null
instead of the raw API envelope so PlanCard receives the correct shape.
- Line 264: Replace the hard-coded query key array used in the useQuery call
(queryKey: ["subscription-current"]) with the canonical query key exported from
"`@/lib/query-keys`"; import the appropriate symbol (e.g., SUBSCRIPTION_CURRENT or
subscriptionCurrentKey) at the top of src/app/dashboard/page.tsx and use that
constant in the queryKey prop so all components share the same contract and
invalidations/refetches can reference the shared key.
- Around line 250-257: Replace the raw fetch usage inside the queryFn for
useQuery (queryKey: queryKeys.walletSummary) with the shared fetchy wrapper from
"`@/lib/fetchy`"; use the same Authorization handling (authToken) and error
handling pattern that other dashboard code uses (e.g., subscription page) so all
HTTP calls go through fetchy — locate the async queryFn in
src/app/dashboard/page.tsx (the block returning res.json()) and refactor it to
call fetchy("/api/transaction/dca-summary", { method: "GET", headers: {
Authorization: `Bearer ${authToken}` } }) and propagate/throw errors
consistently with the repo's fetchy conventions; apply the same replacement in
src/app/dashboard/subscription/page.tsx to ensure both wallet surfaces use the
identical client.

In `@src/app/dashboard/subscription/page.tsx`:
- Around line 272-281: The fetch error handling swallows KYC-block responses so
users never get redirected for verification; update the POST
/api/subscription/payhere-link handling in the async block that calls
redirectToPayHereViaPage so that after parsing result you first check for
result.redirectTo === "/verification" and, if present, call the app's
redirect/route helper to send the user to "/verification" (or
window.location.href = "/verification") before throwing or returning; otherwise
keep the existing check for res.ok and redirectToPayHereViaPage(result.link).
Also ensure the catch block does not suppress navigation — surface the error to
the user or rethrow after any needed redirect.
- Around line 217-246: Replace the manual useEffect fetch that reads
"/api/subscription/current" and calls setSubscription with a TanStack Query:
import useQuery from '`@tanstack/react-query`' and the appropriate query key
factory from '`@/lib/query-keys`' (e.g., getCurrentMembershipKey or
QUERY_KEYS.currentMembership), then implement a useQuery that uses authToken and
the query key, performs the same fetch, transforms the result into the same
shape (using calculateEndDate, packages lookup for planName/price, and fields
like payhere_sub_id, package_id, user_id, created_at, next_billing_date,
is_active), and in onSuccess update the store via setSubscription (or derive UI
directly from the query) instead of the effect; remove the effect and its empty
.catch(), ensure errors other than 404 are surfaced/handled via the query's
onError or error state, and setSelectedPlanId from the query result rather than
the effect.

In `@src/app/page.tsx`:
- Around line 21-22: The assignment to setCount overwrites a real zero because
data.count is using || which treats 0 as falsy; in the fetchy.get call handling
(around fetchy.get<any>("/api/user") and the setCount(...) call) replace the
fallback logic with a nullish check so that setCount uses data.count when it is
0 but falls back to 80 only when data.count is null or undefined (e.g., use the
?? operator or an explicit typeof/data.count === "number" check before calling
setCount).
- Around line 226-234: Restore the navigation handler so the CTAs actually route
to the dashboard: implement goToDashboard to navigate to "/dashboard" and wire
it back into ExistingUserScreen and NewUserScreen (currently passed as
onOpen/onStart). Locate the goToDashboard function and replace the no-op with a
router push call (use the appropriate router hook for this component, e.g.,
useRouter from next/navigation or useRouter from next/router depending on
whether this is a client component), ensuring the component imports the router
hook and calls router.push("/dashboard") inside goToDashboard.

In `@src/components/bottomNavigation.tsx`:
- Line 93: The active-tab check (const isActive = pathname === item.href) is too
strict for nested routes; update the logic in the BottomNavigation/item render
(where isActive is defined) to treat an item as active when the pathname equals
item.href OR when the item is not the root and the pathname starts with the
item's href (e.g., pathname === item.href || (item.href !== '/' &&
pathname.startsWith(item.href + '/'))), ensuring root ("/") still only matches
exactly to avoid false positives on all routes.

In `@src/components/ui/button.tsx`:
- Around line 46-60: Add a safe default button type to avoid implicit submit
behavior: ensure the rendered <button> uses an explicit type defaulting to
"button" while still allowing callers to override it via props (e.g., derive
type from props or use props.type ?? "button"). Update the Button component that
renders <button ref={ref} disabled={disabled || loading} ... {...props}> so the
type attribute is applied explicitly (using the component's props/type value or
default) and avoid spreading props that would be overwritten; reference the
existing button element, props, ref, disabled, loading, variantClasses and
sizeClasses when making the change.

In `@src/components/ui/category-filter.tsx`:
- Around line 19-21: The CategoryFilter and TogglePlan components render plain
<button> elements which default to type="submit" and can inadvertently submit
enclosing forms; update the button elements in
src/components/ui/category-filter.tsx (the button with onClick and cn(...)
className in the CategoryFilter component) and in
src/components/ui/toggle-plan.tsx (the toggle button inside the TogglePlan
component) to include an explicit type="button" attribute so they no longer act
as submit buttons.

In `@src/components/ui/copy-field.tsx`:
- Around line 20-30: The CopyButton renders an icon-only button (using onCopy,
CopyIcon and className) with no accessible name; update the component to add an
accessible label by adding aria-label="Copy" to the button element (or accept a
prop like label/ariaLabel and pass it to the button) so screen readers announce
the action; ensure the default remains "Copy" if no prop is provided and keep
the existing onClick/onCopy behavior.
- Around line 43-49: The clipboard write in handleCopy currently only handles
success; add a .catch(...) to navigator.clipboard.writeText(value) to handle
failures: in the catch block setCopied(false) (to avoid leaving the UI in a
copied state), log or surface the error (e.g., console.error or invoke an
onError handler if provided), and ensure onCopy?.(value) is only called on
success. Update handleCopy to include this error path so denied permissions or
write failures are handled deterministically.

In `@src/components/ui/text-field.tsx`:
- Around line 18-21: The label elements in the TextField component are currently
visual-only; update the TextField (src/components/ui/text-field.tsx) to accept
and pass an id prop to the underlying input/textarea (or generate one via
React's useId if id is not provided), set the label's htmlFor to that id, and
add aria-invalid={!!error} and aria-describedby={error ? `${id}-error` :
undefined} on the control; also ensure the error element uses the matching id
(e.g., `${id}-error`) so assistive tech can associate the label, control, and
error.

In `@src/components/ui/visible-toggle.tsx`:
- Around line 34-46: The toggle and selection controls currently only change
visual styles; make their state available to assistive tech by adding ARIA state
attributes: in VisibleToggle (the button using onToggle and prop visible) add
aria-pressed={visible} (and keep its type="button" and onClick) so screen
readers know it’s a toggle; in the plan card component (PlanCard or whatever
component uses selection/onSelect) expose selection via an appropriate role and
state—e.g., role="radio" or role="button" plus aria-checked={selected} (or
aria-pressed for a toggle-like card) and ensure keyboard activation uses the
same onSelect handler—so the semantic state matches the visual state.
🪄 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

Run ID: 657635cc-4acc-4e72-8b6c-9c8122443670

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdd162 and 6f66d04.

⛔ Files ignored due to path filters (6)
  • bun.lockb is excluded by !**/bun.lockb
  • package-lock.json is excluded by !**/package-lock.json
  • public/BDLogo_Black.svg is excluded by !**/*.svg
  • public/bd-wordmark-light.png is excluded by !**/*.png
  • public/btc-coin-3d.png is excluded by !**/*.png
  • public/gift-emoji-3d.png is excluded by !**/*.png
📒 Files selected for processing (17)
  • package.json
  • src/app/dashboard/page.tsx
  • src/app/dashboard/subscription/page.tsx
  • src/app/dev/page.tsx
  • src/app/page.tsx
  • src/components/LoadingPage.tsx
  • src/components/bottomNavigation.tsx
  • src/components/ui/button.tsx
  • src/components/ui/category-filter.tsx
  • src/components/ui/clickable-card.tsx
  • src/components/ui/copy-field.tsx
  • src/components/ui/page-title.tsx
  • src/components/ui/plan-card.tsx
  • src/components/ui/search-field.tsx
  • src/components/ui/text-field.tsx
  • src/components/ui/toggle-plan.tsx
  • src/components/ui/visible-toggle.tsx

Comment on lines +250 to +257
const { data: summary, isLoading } = useQuery<DCSummary>({
queryKey: queryKeys.walletSummary,
queryFn: async () => {
const res = await fetch("/api/transaction/dca-summary", {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
});
if (!res.ok) throw new Error("Failed to fetch wallet summary");
return res.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the shared HTTP client consistently across src/app/dashboard/page.tsx and src/app/dashboard/subscription/page.tsx.

Both wallet surfaces introduce new raw fetch calls for API traffic even though the repo standardizes on fetchy. That splits request/auth/error behavior across two clients in the same flow and makes future fixes/instrumentation inconsistent across src/app/dashboard/page.tsx and src/app/dashboard/subscription/page.tsx.

As per coding guidelines, "All HTTP requests must use the custom fetchy wrapper from @/lib/fetchy".

🤖 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/app/dashboard/page.tsx` around lines 250 - 257, Replace the raw fetch
usage inside the queryFn for useQuery (queryKey: queryKeys.walletSummary) with
the shared fetchy wrapper from "`@/lib/fetchy`"; use the same Authorization
handling (authToken) and error handling pattern that other dashboard code uses
(e.g., subscription page) so all HTTP calls go through fetchy — locate the async
queryFn in src/app/dashboard/page.tsx (the block returning res.json()) and
refactor it to call fetchy("/api/transaction/dca-summary", { method: "GET",
headers: { Authorization: `Bearer ${authToken}` } }) and propagate/throw errors
consistently with the repo's fetchy conventions; apply the same replacement in
src/app/dashboard/subscription/page.tsx to ensure both wallet surfaces use the
identical client.

Source: Coding guidelines

Comment on lines +263 to +270
const { data: subscription } = useQuery<Subscription | null>({
queryKey: ["subscription-current"],
queryFn: async () => {
const res = await fetch("/api/subscription/current", {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
});
if (!res.ok) return null;
return res.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Normalize /api/subscription/current before passing it to PlanCard.

This query is typed as Subscription | null, but the route returns { subscription, message }, and the inner payload is not the same UI shape that PlanCard reads. As written, the card is consuming the wrong object and will look up fields like planName, price, startDate, and endDate on properties that are not there.

🤖 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/app/dashboard/page.tsx` around lines 263 - 270, The query for useQuery
(queryKey "subscription-current") is returning the API shape { subscription,
message } not the UI Subscription expected by PlanCard, so normalize the
response before passing it down: in the queryFn that fetches
"/api/subscription/current" (used by useQuery) extract the inner subscription
object, map/transform its fields to the Subscription UI shape PlanCard expects
(ensure properties like planName, price, startDate, endDate are present or set
to null/defaults), and return that normalized Subscription | null instead of the
raw API envelope so PlanCard receives the correct shape.

});

const { data: subscription } = useQuery<Subscription | null>({
queryKey: ["subscription-current"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Source the subscription query key from @/lib/query-keys.

Hard-coding ["subscription-current"] makes this cache key local knowledge inside one page. Any invalidation/refetch from other wallet surfaces now has to duplicate the literal instead of sharing the repo’s query-key contract.

As per coding guidelines, "Use TanStack Query for server state with query keys sourced from @/lib/query-keys".

🤖 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/app/dashboard/page.tsx` at line 264, Replace the hard-coded query key
array used in the useQuery call (queryKey: ["subscription-current"]) with the
canonical query key exported from "`@/lib/query-keys`"; import the appropriate
symbol (e.g., SUBSCRIPTION_CURRENT or subscriptionCurrentKey) at the top of
src/app/dashboard/page.tsx and use that constant in the queryKey prop so all
components share the same contract and invalidations/refetches can reference the
shared key.

Source: Coding guidelines

Comment on lines +312 to +319
useEffect(() => {
if (!authToken) return;
fetch("/api/user/kyc/status", {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
})
.then((r) => (r.ok ? r.json() : null))
.then((data) => { if (data && data.status !== "APPROVED") router.push("/verification"); })
.catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when the KYC status check cannot be trusted.

The redirect only happens when a successful response says status !== "APPROVED". If the request returns 401/500 or throws, the code falls through and the wallet still renders, so the KYC gate disappears whenever the check is unavailable.

🤖 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/app/dashboard/page.tsx` around lines 312 - 319, The KYC check currently
only redirects when the fetch succeeds and returns data.status !== "APPROVED",
allowing failures (non-OK responses or errors) to bypass the gate; update the
useEffect fetch to treat any non-ok response or thrown error as untrusted and
redirect to router.push("/verification") — i.e., inspect the response (from
fetch("/api/user/kyc/status") with authToken) and if !r.ok or data is missing or
data.status !== "APPROVED", call router.push("/verification"), and also call
router.push("/verification") in the catch handler to fail closed.

Comment on lines +217 to +246
useEffect(() => {
if (!authToken || packages.length === 0) return;
fetch("/api/subscription/current", {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}` },
})
.then((r) => r.json())
.then((result) => {
if (result.subscription) {
const sub = result.subscription;
const pkg = packages.find((p) => p.id === sub.package_id);
setSubscription({
id: sub.payhere_sub_id,
planName: pkg?.name ?? "Unknown Plan",
planType: sub.frequency ?? "monthly",
price: pkg?.amount ?? 0,
currency: "Rs",
startDate: sub.created_at,
endDate: sub.next_billing_date ?? (pkg ? calculateEndDate(sub.created_at, pkg.type) : sub.updated_at),
isActive: sub.is_active,
packageId: sub.package_id,
userId: sub.user_id,
payhereSubId: sub.payhere_sub_id,
});
setSelectedPlanId(sub.package_id);
} else {
setSubscription(null);
}
})
.catch(() => {});
}, [authToken, packages, setSubscription, calculateEndDate]);

Copy link
Copy Markdown

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

Model current membership as a TanStack query instead of effect-managed store state.

This is server state, but it is fetched in an effect, normalized into the store, and treated as null even when the backend returns a non-404 error. The dashboard page reads the same resource through React Query, so the app now has two different sources of truth for current membership.

As per coding guidelines, "Use TanStack Query for server state with query keys sourced from @/lib/query-keys".

🤖 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/app/dashboard/subscription/page.tsx` around lines 217 - 246, Replace the
manual useEffect fetch that reads "/api/subscription/current" and calls
setSubscription with a TanStack Query: import useQuery from
'`@tanstack/react-query`' and the appropriate query key factory from
'`@/lib/query-keys`' (e.g., getCurrentMembershipKey or
QUERY_KEYS.currentMembership), then implement a useQuery that uses authToken and
the query key, performs the same fetch, transforms the result into the same
shape (using calculateEndDate, packages lookup for planName/price, and fields
like payhere_sub_id, package_id, user_id, created_at, next_billing_date,
is_active), and in onSuccess update the store via setSubscription (or derive UI
directly from the query) instead of the effect; remove the effect and its empty
.catch(), ensure errors other than 404 are surfaced/handled via the query's
onError or error state, and setSelectedPlanId from the query result rather than
the effect.

Source: Coding guidelines

Comment on lines +19 to +21
<button
onClick={onClick}
className={cn(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Shared root cause: implicit submit buttons in reusable controls.
src/components/ui/category-filter.tsx and src/components/ui/toggle-plan.tsx both render <button> elements without type="button", so they can accidentally submit enclosing forms. Set explicit button types in both files.

🤖 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/ui/category-filter.tsx` around lines 19 - 21, The
CategoryFilter and TogglePlan components render plain <button> elements which
default to type="submit" and can inadvertently submit enclosing forms; update
the button elements in src/components/ui/category-filter.tsx (the button with
onClick and cn(...) className in the CategoryFilter component) and in
src/components/ui/toggle-plan.tsx (the toggle button inside the TogglePlan
component) to include an explicit type="button" attribute so they no longer act
as submit buttons.

Comment on lines +20 to +30
<button
onClick={onCopy}
className={cn(
"flex items-center justify-center size-9 rounded-[12px]",
"bg-white border border-[#e2e8f0] transition-colors",
"hover:bg-[#f8fafc] active:bg-[#f1f5f9]",
className
)}
>
<CopyIcon />
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an accessible name to the icon-only copy button.

At Line 20, CopyButton has no text label, so screen readers won’t announce the action. Add aria-label="Copy" (or accept a label prop).

🤖 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/ui/copy-field.tsx` around lines 20 - 30, The CopyButton
renders an icon-only button (using onCopy, CopyIcon and className) with no
accessible name; update the component to add an accessible label by adding
aria-label="Copy" to the button element (or accept a prop like label/ariaLabel
and pass it to the button) so screen readers announce the action; ensure the
default remains "Copy" if no prop is provided and keep the existing
onClick/onCopy behavior.

Comment on lines +43 to +49
const handleCopy = () => {
navigator.clipboard.writeText(value).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
onCopy?.(value);
});
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle clipboard-write failures to avoid silent failure paths.

At Line 44, navigator.clipboard.writeText(value) only handles success. Add a .catch(...) branch so denied permissions/failures don’t leave the UX in an undefined state.

🤖 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/ui/copy-field.tsx` around lines 43 - 49, The clipboard write
in handleCopy currently only handles success; add a .catch(...) to
navigator.clipboard.writeText(value) to handle failures: in the catch block
setCopied(false) (to avoid leaving the UI in a copied state), log or surface the
error (e.g., console.error or invoke an onError handler if provided), and ensure
onCopy?.(value) is only called on success. Update handleCopy to include this
error path so denied permissions or write failures are handled
deterministically.

Comment on lines +18 to +21
{label && (
<label className="text-[12px] font-normal text-[#475569] leading-[16px]">
{label}
</label>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Associate labels/errors with inputs for accessible form semantics.

At Line 19 and Line 62, the <label> elements are visual only. At Line 23 and Line 66, the controls should expose id, aria-invalid, and aria-describedby (when error exists) so assistive tech can connect label/error to the field.

Proposed patch
 const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
   ({ className, state = "default", label, error, disabled, ...props }, ref) => {
     const isDisabled = disabled || state === "disabled";
+    const inputId = props.id ?? props.name;
+    const errorId = error && inputId ? `${inputId}-error` : undefined;

     return (
       <div className="flex flex-col gap-1 w-full">
         {label && (
-          <label className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
+          <label htmlFor={inputId} className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
             {label}
           </label>
         )}
         <input
           ref={ref}
+          id={inputId}
           disabled={isDisabled}
+          aria-invalid={!!error}
+          aria-describedby={errorId}
           className={cn(
@@
         {error && (
-          <p className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
+          <p id={errorId} className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
         )}
       </div>
     );
   }
 );
@@
 const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(
   ({ className, state = "default", label, error, disabled, ...props }, ref) => {
     const isDisabled = disabled || state === "disabled";
+    const inputId = props.id ?? props.name;
+    const errorId = error && inputId ? `${inputId}-error` : undefined;

     return (
       <div className="flex flex-col gap-1 w-full">
         {label && (
-          <label className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
+          <label htmlFor={inputId} className="text-[12px] font-normal text-[`#475569`] leading-[16px]">
             {label}
           </label>
         )}
         <textarea
           ref={ref}
+          id={inputId}
           disabled={isDisabled}
+          aria-invalid={!!error}
+          aria-describedby={errorId}
@@
         {error && (
-          <p className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
+          <p id={errorId} className="text-[12px] text-[`#f13131`] leading-[16px]">{error}</p>
         )}
       </div>
     );
   }
 );

Also applies to: 23-41, 61-65, 66-84

🤖 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/ui/text-field.tsx` around lines 18 - 21, The label elements in
the TextField component are currently visual-only; update the TextField
(src/components/ui/text-field.tsx) to accept and pass an id prop to the
underlying input/textarea (or generate one via React's useId if id is not
provided), set the label's htmlFor to that id, and add aria-invalid={!!error}
and aria-describedby={error ? `${id}-error` : undefined} on the control; also
ensure the error element uses the matching id (e.g., `${id}-error`) so assistive
tech can associate the label, control, and error.

Comment on lines +34 to +46
<button
type="button"
onClick={onToggle}
className={cn(
"flex items-center justify-center w-9 h-9 rounded-[12px] transition-colors",
dark
? "bg-transparent hover:bg-white/5 active:bg-white/10"
: "bg-white border border-[#e2e8f0] drop-shadow-[1px_1px_3px_rgba(203,213,225,0.3)] hover:bg-[#f8fafc] active:bg-[#f1f5f9]",
className
)}
>
{visible ? <EyeOpen dark={dark} /> : <EyeClosed dark={dark} />}
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shared root cause: interactive state is visual-only in src/components/ui/visible-toggle.tsx and src/components/ui/plan-card.tsx.
Both components rely on visual styling to convey toggle/selection state but do not fully expose that state via ARIA. Please add semantic state attributes so the controls are operable and understandable with assistive technologies.

🤖 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/ui/visible-toggle.tsx` around lines 34 - 46, The toggle and
selection controls currently only change visual styles; make their state
available to assistive tech by adding ARIA state attributes: in VisibleToggle
(the button using onToggle and prop visible) add aria-pressed={visible} (and
keep its type="button" and onClick) so screen readers know it’s a toggle; in the
plan card component (PlanCard or whatever component uses selection/onSelect)
expose selection via an appropriate role and state—e.g., role="radio" or
role="button" plus aria-checked={selected} (or aria-pressed for a toggle-like
card) and ensure keyboard activation uses the same onSelect handler—so the
semantic state matches the visual state.

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