-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implement viewing of recurring payments #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis update introduces a recurring payments feature, adding new database schema, server-side router, and UI components for managing and displaying recurring payments. It restructures the payouts section with new layouts and navigation, removes the previous payout tabs component, and adds supporting UI elements such as tooltips and address shortening. Minor import style and dependency updates are also included. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as RecurringPayments UI
participant TRPC as TRPC Client
participant Server
participant DB
User->>UI: Navigates to /payouts/recurring
UI->>TRPC: trpc.recurringPayment.getRecurringRequests()
TRPC->>Server: getRecurringRequests (with user session)
Server->>DB: Query recurringPaymentTable by userId
DB-->>Server: Recurring payment records
Server-->>TRPC: Return payment data
TRPC-->>UI: Payment data
UI-->>User: Render paginated recurring payments table
Possibly related PRs
Suggested reviewers
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
5629c11
to
480266b
Compare
480266b
to
15bbf01
Compare
926c70c
to
1cf64d1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🔭 Outside diff range comments (1)
src/server/context.ts (1)
12-12
:ip
is hard-coded to an empty string
ip
looks intended to hold the requester’s address but is never populated.
Unless another middleware later enriches this field, the value will always be""
, making it effectively useless.- const ip = ""; + const ip = + ctx.req.headers["x-forwarded-for"]?.toString().split(",")[0]?.trim() ?? + ctx.req.socket.remoteAddress ?? + "";Consider extracting it from
x-forwarded-for
orreq.socket.remoteAddress
so downstream logging / rate-limiting has the data it needs.
🧹 Nitpick comments (23)
src/components/ui/tooltip.tsx (1)
11-28
: Provider nesting & re-mounting — hoist the<RadixTooltip.Provider>
Placing a new
Provider
inside every Tooltip instance causes an extra React subtree and resets the delay / skip-delay timers on each hover. Hoisting the provider once (e.g. inapp/layout.tsx
or a shared UI root) keeps the state consistent and avoids unnecessary DOM nodes.-export function Tooltip({ tooltipTrigger, tooltipContent }: TooltipProps) { - return ( - <RadixTooltip.Provider> - <RadixTooltip.Root> +export function Tooltip({ tooltipTrigger, tooltipContent }: TooltipProps) { + return ( + <RadixTooltip.Root> ... - </RadixTooltip.Root> - </RadixTooltip.Provider> + </RadixTooltip.Root> ); }src/components/batch-payout.tsx (1)
253-253
: Full-width container might hurt readability on very wide screens
Removingmax-w-*
allows the card to stretch indefinitely on ultra-wide monitors. If the design intent is to keep the form centred with reasonable line-lengths, consider capping width again (e.g.max-w-6xl
via a breakpoint).src/app/payouts/single/page.tsx (1)
1-5
: Add page-level metadata for SEO / share previews
The new route renders correctly, but exportingmetadata
helps Next.js set<title>
and description.+export const metadata = { + title: "Single Payout • Easy Invoice", + description: "Send a one-off payout with Easy Invoice.", +};src/app/payouts/recurring/page.tsx (1)
1-5
: Expose metadata for the new recurring-payments page+export const metadata = { + title: "Recurring Payments • Easy Invoice", + description: "View and manage all your scheduled payouts.", +};Helps search engines and improves the tab’s label in browsers.
src/components/direct-payout.tsx (2)
394-398
: Guard against undefinedaddress
address?.substring(...)
is safe, but string concatenation will coerceundefined
to the literal"undefined"
, yielding"undefined...undefined"
when the wallet disconnects between renders.-{address?.substring(0, 6)}...{address?.substring(address?.length - 4)} +{address + ? `${address.substring(0, 6)}...${address.substring(address.length - 4)}` + : "Not connected"}Minor, yet prevents an odd UX edge-case.
82-87
: Avoid arbitrary 2 s timeout to flag AppKit readinessRelying on
setTimeout
introduces race conditions (slow networks, long cold-starts).
Prefer listening to an explicit AppKit “ready”/“connected” event if available, or query its actual state in a polling loop.src/app/payouts/recurring/create/page.tsx (1)
1-5
: Consider marking the page as a client component
CreateRecurringPayment
is almost certainly interactive; wrapping it inside a server component is fine, but keeping the entire file client-side avoids React ↔ Server Component boundary surprises:+'use client';
Not mandatory—RSCs can embed client components—but adding the directive keeps mental overhead low.
src/app/payouts/recurring/layout.tsx (1)
3-14
: Potential double RSC ↔ Client boundary
RecurringPaymentsNavigation
is interactive (tabs). If it’s already marked'use client'
, importing it inside an RSC is legal but creates an extra boundary that prevents automatic revalidation & hooks sharing.If you expect the whole recurring payout area to be interactive, declare the layout itself as a client component:
+'use client';
Otherwise, no action needed.
src/trpc/server.ts (1)
14-14
: Minor import-ordering nitType imports (
type AppRouter
) placed before value imports (appRouter
) is great for clarity; consider grouping allimport type
lines together to keep lint happy (if@typescript-eslint/consistent-type-imports
is enabled).src/server/routers/recurring-payment.ts (1)
8-11
: Pagination missing – potential large result sets
findMany
withoutlimit/offset
will return every recurring payment for the user. Once the table grows this could hurt latency and memory. Recommend cursor-based or page-size-capped pagination before this goes to production.src/components/create-recurring-payment.tsx (1)
11-14
: Add accessible text for the icon
Screen-reader users will hear nothing for the standalonePlus
icon. Wrap it in a visually-hiddenspan
or providearia-label
/title
.- <Plus className="h-5 w-5" /> + <Plus className="h-5 w-5" aria-hidden="true" /> + <span className="sr-only">Create</span>src/app/payouts/page.tsx (1)
4-7
: Metadata description outdated
The description still references “single or batch” payouts only. Now that recurring payouts exist, update to avoid misleading search-engine snippets.src/components/recurring-payments-navigation.tsx (1)
12-18
: Consider making the pathname check stricter
pathname.includes("/create")
will also match URLs such as/payouts/recurring/create-something
or even/blog/how-to-create-recurring
if the component is rendered in another context.
A more explicit check (e.g.pathname.endsWith("/create")
or comparing against a predefined set of exact routes) would avoid accidental matches.src/components/short-address.tsx (2)
17-20
: Add error-handling around clipboard operations
navigator.clipboard.writeText
can fail (e.g. insecure context, user blocks permission).
Wrapping the call intry/catch
prevents an uncaught promise rejection and lets you surface a helpful toast message.- const handleCopy = () => { - navigator.clipboard.writeText(address); - toast.success("Address copied to clipboard"); - }; + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(address); + toast.success("Address copied to clipboard"); + } catch (err) { + console.error("Clipboard copy failed", err); + toast.error("Failed to copy address"); + } + };
13-16
: Guard against unexpectedly short addresses
substring
assumes the address is at least 10 chars long.
For robustness, bail out (or return the raw string) if the length check fails.src/components/payout-navigation.tsx (1)
12-20
: Edge-case: “/payouts” root pathIf a user lands on
/payouts
(no subtype), none of theincludes
checks match/single
,/batch
, or/recurring
.
Today this falls through to “single”, which is fine but implicit.
Consider redirecting/payouts
to/payouts/single
at the route level or making that assumption explicit in a comment.src/app/payouts/layout.tsx (1)
13-15
: Handle session-fetch errors explicitly
getCurrentSession()
may throw (network issues, auth service down).
Atry/catch
with a fallback redirect (or error page) would prevent the whole layout from crashing.src/components/view-recurring-payments/blocks/completed-payments.tsx (2)
20-24
: Sort payments before slicing to ensure newest-first displayIf the
payments
array isn’t pre-sorted, slicing first can surface arbitrary items.
Sort bydate
(descending) before applying theisExpanded
logic.- const visiblePayments = isExpanded ? payments : payments.slice(0, 2); + const ordered = [...payments].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ); + const visiblePayments = isExpanded ? ordered : ordered.slice(0, 2);
30-41
: Userel="noreferrer"
when linking to external explorersAdding
noreferrer
alongsidenoopener
removes theReferer
header, preventing the external site from seeing the origin URL.- rel="noopener noreferrer" + rel="noopener noreferrer"(If you intentionally keep
Referer
, feel free to ignore.)src/components/view-recurring-payments/view-recurring-payments.tsx (3)
27-28
: ResetcurrentPage
when the incoming dataset changesIf the user is on page > 1 and a refetch returns fewer items (e.g., after cancelling a payment),
currentPage
may exceedtotalPages
, rendering an empty table. Resetting or clamping the page inside anuseEffect
tied torecurringPayments?.length
prevents this edge-case.Also applies to: 67-75
106-109
: Pass theDate
object directly toformatDate
instead oftoString()
Converting first to string discards timezone information and forces
formatDate
to re-parse the value, adding unnecessary overhead and potential locale issues.-? formatDate(payment.recurrence.startDate.toString()) +? formatDate(payment.recurrence.startDate)
157-180
: Minor UX: disable pagination buttons only when necessaryToday both buttons are always rendered; disabling is correct, but hiding the entire pagination bar when
totalPages === 1
avoids redundant UI.src/server/db/schema.ts (1)
59-67
: Enum namefrequency_enum
may clash with existing enumsYou already store recurrence frequency in the
request
table as a plain string. Introducing a new Postgres enum with the same semantic meaning can make future migrations harder. Confirm that a duplicate enum is intended and that all write paths use the same canonical list.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (28)
drizzle.config.ts
(1 hunks)package.json
(1 hunks)src/app/payouts/batch/page.tsx
(1 hunks)src/app/payouts/layout.tsx
(1 hunks)src/app/payouts/page.tsx
(1 hunks)src/app/payouts/recurring/create/page.tsx
(1 hunks)src/app/payouts/recurring/layout.tsx
(1 hunks)src/app/payouts/recurring/page.tsx
(1 hunks)src/app/payouts/single/page.tsx
(1 hunks)src/components/batch-payout.tsx
(1 hunks)src/components/create-recurring-payment.tsx
(1 hunks)src/components/direct-payout.tsx
(1 hunks)src/components/payout-navigation.tsx
(1 hunks)src/components/payout-tabs.tsx
(0 hunks)src/components/recurring-payments-navigation.tsx
(1 hunks)src/components/short-address.tsx
(1 hunks)src/components/ui/tooltip.tsx
(1 hunks)src/components/view-recurring-payments/blocks/completed-payments.tsx
(1 hunks)src/components/view-recurring-payments/blocks/frequency-badge.tsx
(1 hunks)src/components/view-recurring-payments/blocks/status-badge.tsx
(1 hunks)src/components/view-recurring-payments/view-recurring-payments.tsx
(1 hunks)src/server/context.ts
(1 hunks)src/server/db/schema.ts
(4 hunks)src/server/index.ts
(1 hunks)src/server/routers/recurring-payment.ts
(1 hunks)src/trpc/react.tsx
(3 hunks)src/trpc/server.ts
(1 hunks)src/trpc/shared.ts
(1 hunks)
💤 Files with no reviewable changes (1)
- src/components/payout-tabs.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Build
🔇 Additional comments (14)
package.json (1)
34-34
: Radix Tooltip dependency looks good — ensure lock-file is regeneratedThe addition of
@radix-ui/react-tooltip
aligns with the new Tooltip component. Just remember to runnpm i
(or the project’s package-manager equivalent) so that the lock-file is updated in the same commit/PR, avoiding CI drift.src/trpc/shared.ts (1)
1-4
: Style-only import tweak is correctSwitching to
import type
keeps the compiled bundle lean and avoids inadvertent value imports. No further action required.src/app/payouts/batch/page.tsx (1)
1-5
: Confirm component type — needs"use client"
ifBatchPayout
is a client component
app/
pages are server components by default in Next 14. IfBatchPayout
uses client-side hooks (useState
,react-hook-form
, etc.), this page must opt-in to the client runtime:+ "use client"; import { BatchPayout } from "@/components/batch-payout"; export default function BatchPayoutSlot() { return <BatchPayout />; }
Please verify; otherwise Next.js will throw an RSC violation at build time.
src/trpc/react.tsx (2)
9-9
: Type-only import is the right call
Switching toimport type { … }
prevents theAppRouter
type from ending up in the JS bundle—good for tree-shaking and avoids a future ESM/CJS mismatch warning.
28-28
: Stylistic comma tweak only
The extra trailing commas are harmless and consistent with Prettier defaults. No action required.Also applies to: 50-50
src/server/index.ts (1)
6-6
: Router wiring looks correct
recurringPaymentRouter
is imported and exposed asrecurringPayment
—consistent with the front-end call patternapi.recurringPayment.*
.Also applies to: 14-14
src/server/context.ts (1)
2-3
: 👍 Type-only imports are a good callSwitching
trpc
/trpcNext
toimport type
keeps the runtime bundle leaner and avoids accidental value-level imports.src/components/direct-payout.tsx (1)
186-186
: Full-width card may hurt readability on large screensDropping
max-w-2xl
means the payment form can now stretch beyond ~640 px.
On wide monitors this produces extremely long input rows and dotted focus outlines. Please confirm the parent layout imposes another width cap, or consider a responsive max-width:- <div className="flex justify-center mx-auto w-full"> + <div className="flex justify-center mx-auto w-full max-w-[52rem]">src/trpc/server.ts (1)
10-10
: 👍 ConvertingTRPCErrorResponse
to a type-only importKeeps build output slimmer and matches TypeScript 5 recommendations.
src/components/create-recurring-payment.tsx (1)
8-10
: Minor layout redundancy
<div className="flex … w-full max-w-6xl">
wraps a<Card className="w-full">
; the firstw-full
is unnecessary.src/app/payouts/page.tsx (1)
9-11
:return redirect()
is redundant
redirect()
throwsRedirectType
and never returns; the extrareturn
is harmless but superfluous.src/components/view-recurring-payments/blocks/frequency-badge.tsx (1)
6-11
: LGTM – clear colour mapping
The frequency-to-colour mapping is straightforward and type-safe.src/components/view-recurring-payments/blocks/status-badge.tsx (1)
18-22
: Capitalisation handled nicely
Good use ofcharAt(0).toUpperCase()
for presentation; no issues spotted.src/server/db/schema.ts (1)
233-238
: Potential incompatibility: storingDate
inside a JSON columnDrizzle will serialise
Date
to an ISO string on insert, returning it asstring
on select.
Declaring the type asDate
can therefore break type-safety (toISOString
etc.). Consider usingstring
consistently or switch to atimestamp
column if you need nativeDate
handling.
const recurringRequests = await db.query.recurringPaymentTable.findMany({ | ||
where: and(eq(recurringPaymentTable.userId, user?.id as string)), | ||
orderBy: desc(recurringPaymentTable.createdAt), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider removing redundant and()
wrapper & assert user.id
presence
and()
with a single eq()
clause adds no value and slightly clutters the query; Drizzle accepts the bare eq()
condition.
Also, user
is guaranteed by protectedProcedure
, so the non-null assertion can be avoided in favour of an explicit runtime guard (throw 401) or by refining the procedure’s input type to make user.id
non-nullable.
- where: and(eq(recurringPaymentTable.userId, user?.id as string)),
+ where: eq(recurringPaymentTable.userId, user.id),
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const recurringRequests = await db.query.recurringPaymentTable.findMany({ | |
where: and(eq(recurringPaymentTable.userId, user?.id as string)), | |
orderBy: desc(recurringPaymentTable.createdAt), | |
}); | |
const recurringRequests = await db.query.recurringPaymentTable.findMany({ | |
where: eq(recurringPaymentTable.userId, user.id), | |
orderBy: desc(recurringPaymentTable.createdAt), | |
}); |
🤖 Prompt for AI Agents
In src/server/routers/recurring-payment.ts around lines 8 to 11, remove the
redundant and() wrapper around the single eq() condition in the query to
simplify it. Also, since user is guaranteed by protectedProcedure, add an
explicit runtime check to ensure user.id is present and throw a 401 error if
not, instead of using a non-null assertion. Alternatively, refine the
procedure’s input type to make user.id non-nullable to avoid the assertion.
src/components/view-recurring-payments/view-recurring-payments.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (6)
src/app/payouts/single/page.tsx (2)
3-6
: Explicitly typemetadata
for stronger compile-time safety
metadata
is currently inferred as a plain object. Importing and annotating it withMetadata
fromnext
gives the compiler a chance to warn if any required keys are missing or mis-typed.+import type { Metadata } from "next"; -export const metadata = { +export const metadata: Metadata = { title: "Single Payouts | Easy Invoice", description: "Create one time payments using Easy Invoice", };
7-9
: Consider converting this to a client component only if needed
SinglePayoutPage
is a server component that directly renders theDirectPayment
client component.
That’s perfectly valid, but it forces an extra RSC/Client boundary which slightly inflates the bundle.
IfSinglePayoutPage
itself doesn’t do any server-only work (data fetching, auth, etc.), adding"use client"
at the top and making the whole file a client component would avoid the extra boundary.No change required—just pointing out a potential micro-optimisation.
src/app/payouts/page.tsx (1)
10-12
: Callredirect()
directly instead of returning it
redirect()
fromnext/navigation
has the return typenever
; it immediately throws a Response that Next.js catches.
Usingreturn redirect(...)
is harmless but a bit noisy and can confuse editors/linters about the function’s return type.
Invokingredirect()
by itself is the idiomatic pattern shown in the docs.export default function PayoutsPage() { - return redirect("/payouts/single"); + redirect("/payouts/single"); }drizzle/0006_dry_wendell_vaughn.sql (2)
1-2
: Idempotent enum creation
PostgreSQL supportsCREATE TYPE IF NOT EXISTS
; using it ensures the migration won’t error if the enum already exists. Alternatively, wrap these statements in aDO
block catchingduplicate_object
.
3-16
: Optimize queries and validate data types
- Add an index on
"userId"
(and optionally on"status"
) to speed up lookups by user or status.- Storing monetary values as
text
may complicate calculations—consider usingnumeric
fortotalAmountPerMonth
.src/app/layout.tsx (1)
41-46
: Consider hoisting<Toaster />
out of the tooltip provider
Toaster
doesn’t use tooltip context, so nesting it adds an unnecessary re-render whenever tooltip settings change.- <TooltipProvider> - <TRPCReactProvider cookies={cookies().toString()}> - <BackgroundWrapper>{children}</BackgroundWrapper> - </TRPCReactProvider> - <Toaster /> - </TooltipProvider> + <TooltipProvider> + <TRPCReactProvider cookies={cookies().toString()}> + <BackgroundWrapper>{children}</BackgroundWrapper> + </TRPCReactProvider> + </TooltipProvider> + <Toaster />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
drizzle.config.ts
(1 hunks)drizzle/0006_dry_wendell_vaughn.sql
(1 hunks)drizzle/meta/0006_snapshot.json
(1 hunks)drizzle/meta/_journal.json
(1 hunks)src/app/layout.tsx
(2 hunks)src/app/payouts/batch/page.tsx
(1 hunks)src/app/payouts/page.tsx
(1 hunks)src/app/payouts/recurring/create/page.tsx
(1 hunks)src/app/payouts/recurring/layout.tsx
(1 hunks)src/app/payouts/recurring/page.tsx
(1 hunks)src/app/payouts/single/page.tsx
(1 hunks)src/components/ui/tooltip.tsx
(1 hunks)src/components/view-recurring-payments/view-recurring-payments.tsx
(1 hunks)src/server/db/schema.ts
(4 hunks)src/server/routers/recurring-payment.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- drizzle/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (9)
- src/app/payouts/recurring/layout.tsx
- src/components/ui/tooltip.tsx
- drizzle.config.ts
- src/app/payouts/batch/page.tsx
- src/server/routers/recurring-payment.ts
- src/app/payouts/recurring/page.tsx
- src/app/payouts/recurring/create/page.tsx
- src/components/view-recurring-payments/view-recurring-payments.tsx
- src/server/db/schema.ts
🧰 Additional context used
🪛 GitHub Actions: Build and Lint
src/app/layout.tsx
[error] 1-1: Module not found: Can't resolve '@radix-ui/react-tooltip'. Build failed due to missing module dependency.
🔇 Additional comments (5)
drizzle/0006_dry_wendell_vaughn.sql (1)
18-22
: Graceful foreign key constraint handling
TheDO … EXCEPTION
block correctly ignores duplicate constraint errors, making this migration idempotent.drizzle/meta/0006_snapshot.json (3)
361-459
: Recurring payment table in snapshot
Theeasyinvoice_recurring_payment
table, its columns, defaults, and theuserId
foreign key are accurately captured in the snapshot.
787-791
: Frequency enum in snapshot
Thefrequency_enum
values (DAILY
,WEEKLY
,MONTHLY
,YEARLY
) align with the migration script.
812-815
: Recurring payment status enum in snapshot
Therecurring_payment_status
enum (pending
,active
,paused
,completed
,cancelled
) matches the migration definition.src/app/layout.tsx (1)
7-7
: ```shellCheck if @radix-ui/react-tooltip is recorded in package-lock.json
grep -R '"@radix-ui/react-tooltip":' -n package-lock.json
</details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
Changes
New stuff
ShortAddress
component which display an address with...
in between and can be copied to clipboard.Tooltip
component that used radix UIReworked
payouts/single
,payouts/batch
,payouts/recurring
,payouts/recurring/create
Testing
Before you check this out, please run


npm run db:push
.Then you copy the contents of this file
recurring_payment_inserts.txt
into Drizzle Studio's SQL Editor, run it and verify that you get the data inserted:
After that check the following cases
/payouts
redirects you to/payouts/single
/payouts/batch
/payouts/recurring
works both via clicking the tab and going to the URL directly./payouts/recurring/create
. Switching between the two subtabs should also work normally and the URL should reflect the changes.Resolves #19
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores
@radix-ui/react-tooltip
dependency.Refactor
/payouts
page with redirect to/payouts/single
.Documentation
Tests
Revert