-
Notifications
You must be signed in to change notification settings - Fork 2.7k
.link
domain renewals
#2698
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
.link
domain renewals
#2698
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Warning Rate limit exceeded@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 57 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (2)
WalkthroughThis update introduces comprehensive support for domain renewal billing and automation. It adds new cron jobs for sending renewal reminders and processing renewal payments, implements invoice management for domain renewals, and enhances Stripe webhook handling for various invoice events. The UI is updated to display and manage domain auto-renewal, and several new React email templates are provided for domain renewal notifications. Schema changes support tracking renewal fees and auto-renewal status. Changes
Sequence Diagram(s)Domain Renewal Payment Cron FlowsequenceDiagram
participant Vercel Cron
participant API /renewal-payments
participant DB
participant Stripe
Vercel Cron->>API /renewal-payments: GET /api/cron/domains/renewal-payments
API /renewal-payments->>API /renewal-payments: Verify signature
API /renewal-payments->>DB: Query domains expiring in 14 days
API /renewal-payments->>DB: Group domains by workspace, create invoices
loop For each invoice
API /renewal-payments->>Stripe: Create payment intent (if Stripe customer)
end
API /renewal-payments-->>Vercel Cron: JSON response (OK or error)
Domain Auto-Renewal Toggle (UI & API)sequenceDiagram
participant User
participant DomainCard UI
participant DomainAutoRenewalModal
participant API /domains/[domain]
participant Dynadot API
User->>DomainCard UI: Click "Enable/Disable Auto-Renew"
DomainCard UI->>DomainAutoRenewalModal: Open modal
User->>DomainAutoRenewalModal: Confirm action
DomainAutoRenewalModal->>API /domains/[domain]: PATCH autoRenew
API /domains/[domain]->>Dynadot API: Update renew option
Dynadot API-->>API /domains/[domain]: Success/failure
API /domains/[domain]-->>DomainAutoRenewalModal: Response
DomainAutoRenewalModal-->>DomainCard UI: Update status
Stripe Webhook: Charge Succeeded (Domain Renewal)sequenceDiagram
participant Stripe
participant Webhook Handler
participant DB
participant Email Service
Stripe->>Webhook Handler: charge.succeeded event
Webhook Handler->>DB: Fetch invoice by ID
Webhook Handler->>DB: Update invoice status, payment info
Webhook Handler->>DB: Extend domain expiration, enable auto-renew
Webhook Handler->>Email Service: Send "Domain Renewed" email
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90 minutes Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 2
♻️ Duplicate comments (19)
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx (2)
45-55
: Add error handling for PDF generationThe PDF generation functions could throw errors that aren't currently handled. Consider wrapping these calls in try-catch blocks to provide better error reporting.
let pdf: Buffer | null = null; - if (invoice.type === "partnerPayout") { - pdf = await PartnerPayoutInvoice({ - invoice, - workspace: invoice.workspace, - }); - } else if (invoice.type === "domainRenewal") { - pdf = await DomainRenewalInvoice({ - invoice, - workspace: invoice.workspace, - }); - } + try { + if (invoice.type === "partnerPayout") { + pdf = await PartnerPayoutInvoice({ + invoice, + workspace: invoice.workspace, + }); + } else if (invoice.type === "domainRenewal") { + pdf = await DomainRenewalInvoice({ + invoice, + workspace: invoice.workspace, + }); + } + } catch (error) { + throw new DubApiError({ + code: "internal_server_error", + message: `Failed to generate PDF for invoice ${invoiceId}: ${error.message}`, + }); + }
57-62
: Improve error message accuracyThe error message "Invoice not found" is misleading since the invoice was successfully retrieved from the database. The actual issue is that the PDF generation failed or the invoice type is unsupported.
if (!pdf) { throw new DubApiError({ code: "bad_request", - message: `Invoice ${invoiceId} not found in workspace.`, + message: `Failed to generate PDF for invoice ${invoiceId}. Unsupported invoice type: ${invoice.type}`, }); }apps/web/lib/dynadot/set-renew-option.ts (1)
52-60
: Document fire-and-forget error handling behaviorThe function catches errors without re-throwing them, making this a fire-and-forget operation. Consider documenting this behavior or re-throwing critical errors after logging.
Add a comment explaining the fire-and-forget behavior:
} catch (error) { + // Log error but don't throw - this is a fire-and-forget operation + // to avoid blocking the main workflow if Dynadot API is unavailable await log({ message: `Failed to set renew option for ${domain}: ${error.message}`, type: "errors", mention: true, }); console.error(error); }apps/web/app/(ee)/api/stripe/webhook/charge-refunded.ts (1)
35-46
: Consider type safety for registeredDomains fieldThe casting of
invoice.registeredDomains as string[]
could be unsafe if the database contains invalid data. Consider adding runtime validation.async function processDomainRenewalInvoice({ invoice }: { invoice: Invoice }) { - const domains = invoice.registeredDomains as string[]; + const domains = Array.isArray(invoice.registeredDomains) + ? invoice.registeredDomains.filter((domain): domain is string => typeof domain === 'string') + : []; + if (domains.length === 0) { + console.log(`No valid domains found in invoice ${invoice.id}`); + return; + } await Promise.allSettled( domains.map((domain) => setRenewOption({ domain, autoRenew: false, }), ), ); }packages/email/src/templates/domain-renewal-failed.tsx (1)
80-85
: Fix domain link URLsThe domain links only use the slug as the href, which won't form valid URLs. They should include the protocol.
<Link - href={domain.slug} + href={`https://${domain.slug}`} className="font-semibold text-black underline" > {domain.slug} </Link>packages/email/src/templates/domain-renewed.tsx (1)
68-73
: Fix domain link URLsSimilar to the renewal failed template, the domain links only use the slug without the protocol, which won't form valid URLs.
<Link - href={domain.slug} + href={`https://${domain.slug}`} className="font-semibold text-black underline" > {domain.slug} </Link>apps/web/app/(ee)/api/cron/domains/renewal-reminders/route.ts (2)
38-46
: Use addDays for clarityUsing
subDays
with a negative value is confusing. UseaddDays
for better readability.const targetDates = REMINDER_WINDOWS.map((days) => { - const date = subDays(now, -days); + const date = addDays(now, days); return { start: startOfDay(date), end: endOfDay(date), days, }; });
111-125
: Add error handling for batch email sendingThe batch send operation could partially fail. Consider adding error handling to track which emails failed.
for (const reminderDomainsChunk of reminderDomainsChunks) { - await resend.batch.send( - reminderDomainsChunk.map(({ workspace, user, domain }) => ({ - from: VARIANT_TO_FROM_MAP.notifications, - to: user.email!, - subject: "Your domain is expiring soon", - variant: "notifications", - react: DomainRenewalReminder({ - email: user.email!, - workspace, - domain, - }), - })), - ); + try { + await resend.batch.send( + reminderDomainsChunk.map(({ workspace, user, domain }) => ({ + from: VARIANT_TO_FROM_MAP.notifications, + to: user.email!, + subject: "Your domain is expiring soon", + variant: "notifications", + react: DomainRenewalReminder({ + email: user.email!, + workspace, + domain, + }), + })), + ); + } catch (error) { + await log({ + message: `Failed to send batch reminder emails: ${error.message}`, + type: "errors", + }); + // Continue with next batch + } }packages/email/src/templates/domain-expired.tsx (1)
83-83
: Fix the domain link href to use absolute URLThe
href
attribute should use an absolute URL with protocol, not just the domain slug. This will create a relative link instead of linking to the actual domain.- href={domain.slug} + href={`https://${domain.slug}`}packages/email/src/templates/domain-renewal-reminder.tsx (1)
62-62
: Fix the domain link href to use absolute URLThe
href
attribute should use an absolute URL with protocol for proper linking to the domain.- href={domain.slug} + href={`https://${domain.slug}`}apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx (2)
37-47
: Add type validation for registeredDomainsThe code assumes
invoice.registeredDomains
is a string array, but the schema defines it asJson?
. Consider adding runtime validation or type assertions.+ // Validate registeredDomains is an array of strings + const domainSlugs = Array.isArray(invoice.registeredDomains) + ? invoice.registeredDomains.filter((d): d is string => typeof d === 'string') + : []; + const domains = await prisma.registeredDomain.findMany({ where: { slug: { - in: invoice.registeredDomains as string[], + in: domainSlugs, }, }, select: { slug: true, renewalFee: true, }, });
111-111
: Prevent division by zero errorThe platform fee percentage calculation could throw a division by zero error if
invoice.amount
is 0.- label: `Platform fees (${Math.round((invoice.fee / invoice.amount) * 100)}%)`, + label: `Platform fees${invoice.amount > 0 ? ` (${Math.round((invoice.fee / invoice.amount) * 100)}%)` : ''}`,apps/web/ui/modals/domain-auto-renewal-modal.tsx (2)
66-68
: Add runtime guard for optionalregisteredDomain
before usingexpiresAt
andrenewalFee
The
DomainProps
definition marksregisteredDomain
as optional, so it's possible fordomain.registeredDomain
to beundefined
. Your current code extracts properties without first checking thatregisteredDomain
exists.Add a guard before extracting properties:
+ if (!domain.registeredDomain) { + return ( + <Modal + showModal={showDomainAutoRenewalModal} + setShowModal={setShowDomainAutoRenewalModal} + > + <div className="p-6 text-center"> + <p className="text-sm text-neutral-600"> + This domain is not registered and cannot be renewed. + </p> + </div> + </Modal> + ); + } + const isEnabling = enableAutoRenewal; const expiresAt = domain.registeredDomain?.expiresAt; const renewalFee = domain.registeredDomain?.renewalFee;
82-100
: Remove non-null assertions after adding proper null checksLines 86-87 and 93 use non-null assertions (
!
) which could cause runtime errors if the values are undefined. After adding the null check forregisteredDomain
suggested above, these assertions become safe to use.Once the guard is in place, update the code to remove the assertions:
{isEnabling ? ( <> By enabling auto-renewal, Dub will automatically renew your domain - on <strong>{formatDate(expiresAt!)}</strong> for{" "} - {currencyFormatter(renewalFee! / 100)}. + on <strong>{formatDate(expiresAt)}</strong> for{" "} + {currencyFormatter(renewalFee / 100)}. </> ) : ( <> By disabling auto-renewal, your domain{" "} <strong>{domain.slug}</strong> will expire on{" "} - <strong>{formatDate(expiresAt!)}</strong>. + <strong>{formatDate(expiresAt)}</strong>.apps/web/lib/stripe/create-payment-intent.ts (1)
31-41
: Improve error handling consistencyThe current implementation returns early without proper error responses, while the main try-catch block uses structured logging. This creates inconsistent error handling.
if (cards.data.length === 0 && links.data.length === 0) { - console.error(`No valid payment methods found for customer ${stripeId}.`); - return; + const errorMsg = `No valid payment methods found for customer ${stripeId}.`; + console.error(errorMsg); + await log({ + message: errorMsg, + type: "errors", + mention: true, + }); + throw new Error(errorMsg); } const paymentMethod = cards.data[0] || links.data[0]; if (!paymentMethod) { - console.error(`No valid payment method found for customer ${stripeId}.`); - return; + const errorMsg = `No valid payment method found for customer ${stripeId}.`; + console.error(errorMsg); + await log({ + message: errorMsg, + type: "errors", + mention: true, + }); + throw new Error(errorMsg); }apps/web/app/(ee)/api/cron/domains/renewal-payments/route.ts (1)
141-146
: Ensure safe error message extractionThe error might not have a
message
property. Use a type guard or fallback.} catch (error) { await log({ - message: "Domains renewal cron failed. Error: " + error.message, + message: "Domains renewal cron failed. Error: " + (error instanceof Error ? error.message : String(error)), type: "errors", });apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx (1)
151-151
: Prevent potential division by zeroThe platform fee percentage calculation could result in division by zero if
invoice.amount
is 0.- label: `Platform fees (${Math.round((invoice.fee / invoice.amount) * 100)}%)`, + label: `Platform fees${invoice.amount > 0 ? ` (${Math.round((invoice.fee / invoice.amount) * 100)}%)` : ''}`,apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts (1)
182-182
: Use consistent amount source for payout emailsThe email uses
charge.amount
instead ofinvoice.amount
. This could lead to inconsistencies if there are currency conversions or processing fees.apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)
155-169
: Add error handling for email sendingThe email sending operation lacks error handling, which could cause silent failures.
🧹 Nitpick comments (4)
apps/web/lib/stripe/create-payment-intent.ts (1)
58-63
: Simplify idempotency key handlingThe conditional object spread can be simplified for better readability.
}, - { - ...(idempotencyKey - ? { - idempotencyKey, - } - : {}), - }, + idempotencyKey ? { idempotencyKey } : undefined,apps/web/app/(ee)/api/cron/domains/renewal-payments/route.ts (1)
129-131
: Remove unnecessary non-null assertion.The
stripeId
is already verified to exist before this point, so the non-null assertion is redundant.await createPaymentIntent({ - stripeId: workspace.stripeId!, + stripeId: workspace.stripeId, amount: invoice.total,packages/prisma/schema/domain.prisma (1)
34-34
: Consider making the renewal fee configurable.The renewal fee is hardcoded to 1200 ($12). This might need to be configurable in the future to support different pricing tiers or promotional rates.
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx (1)
77-93
: Simplify period calculation logic.The reduce operations for finding earliest and latest periods could be simplified for better readability.
- const earliestPeriodStart = payouts.reduce( - (acc, payout) => { - if (!acc) return payout.periodStart; - if (!payout.periodStart) return acc; - return payout.periodStart < (acc as Date) ? payout.periodStart : acc; - }, - null as Date | null, - ); + const validPeriodStarts = payouts + .map(p => p.periodStart) + .filter((d): d is Date => d !== null); + const earliestPeriodStart = validPeriodStarts.length > 0 + ? new Date(Math.min(...validPeriodStarts.map(d => d.getTime()))) + : null; - const latestPeriodEnd = payouts.reduce( - (acc, payout) => { - if (!acc) return payout.periodEnd; - if (!payout.periodEnd) return acc; - return payout.periodEnd > (acc as Date) ? payout.periodEnd : acc; - }, - null as Date | null, - ); + const validPeriodEnds = payouts + .map(p => p.periodEnd) + .filter((d): d is Date => d !== null); + const latestPeriodEnd = validPeriodEnds.length > 0 + ? new Date(Math.max(...validPeriodEnds.map(d => d.getTime()))) + : null;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
apps/web/app/(ee)/api/admin/payouts/route.ts
(2 hunks)apps/web/app/(ee)/api/cron/domains/renewal-payments/route.ts
(1 hunks)apps/web/app/(ee)/api/cron/domains/renewal-reminders/route.ts
(1 hunks)apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts
(1 hunks)apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
(3 hunks)apps/web/app/(ee)/api/stripe/webhook/charge-refunded.ts
(1 hunks)apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
(2 hunks)apps/web/app/(ee)/api/stripe/webhook/route.ts
(3 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx
(1 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx
(1 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
(2 hunks)apps/web/app/api/domains/[domain]/route.ts
(4 hunks)apps/web/app/api/workspaces/[idOrSlug]/billing/invoices/route.ts
(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx
(1 hunks)apps/web/lib/dynadot/set-renew-option.ts
(1 hunks)apps/web/lib/stripe/create-payment-intent.ts
(1 hunks)apps/web/lib/types.ts
(1 hunks)apps/web/lib/zod/schemas/domains.ts
(1 hunks)apps/web/ui/domains/add-edit-domain-form.tsx
(6 hunks)apps/web/ui/domains/domain-card.tsx
(9 hunks)apps/web/ui/modals/domain-auto-renewal-modal.tsx
(1 hunks)apps/web/vercel.json
(1 hunks)packages/email/src/templates/domain-expired.tsx
(1 hunks)packages/email/src/templates/domain-renewal-failed.tsx
(1 hunks)packages/email/src/templates/domain-renewal-reminder.tsx
(1 hunks)packages/email/src/templates/domain-renewed.tsx
(1 hunks)packages/prisma/schema/domain.prisma
(1 hunks)packages/prisma/schema/invoice.prisma
(1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: paypal webhook verification in the dub codebase is handled at the route level in `apps/web/app/(ee)/...
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-19T01:46:45.723Z
Learning: PayPal webhook verification in the Dub codebase is handled at the route level in `apps/web/app/(ee)/api/paypal/webhook/route.ts` using the `verifySignature` function. Individual webhook handlers like `payoutsItemFailed` don't need to re-verify signatures since they're only called after successful verification.
Applied to files:
apps/web/app/(ee)/api/stripe/webhook/route.ts
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
apps/web/app/(ee)/api/stripe/webhook/charge-refunded.ts
apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
📚 Learning: in the singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in th...
Learnt from: devkiran
PR: dubinc/dub#2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
Applied to files:
apps/web/app/(ee)/api/stripe/webhook/route.ts
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
📚 Learning: in apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward conditi...
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/api/admin/payouts/route.ts
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx
apps/web/ui/modals/domain-auto-renewal-modal.tsx
apps/web/ui/domains/add-edit-domain-form.tsx
📚 Learning: in the partnerprogramsummary email template (packages/email/src/templates/partner-program-summary.ts...
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx
📚 Learning: in the dub codebase, multiple payout records can now share the same stripetransferid because payouts...
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Applied to files:
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
📚 Learning: the `/api/workspaces/${slug}/billing/payment-methods` post endpoint in the billing api returns eithe...
Learnt from: devkiran
PR: dubinc/dub#2433
File: apps/web/ui/modals/add-payment-method-modal.tsx:60-62
Timestamp: 2025-05-29T09:49:19.604Z
Learning: The `/api/workspaces/${slug}/billing/payment-methods` POST endpoint in the billing API returns either an error (handled by response.ok check) or a response object containing a `url` property for successful requests.
Applied to files:
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts
apps/web/app/api/workspaces/[idOrSlug]/billing/invoices/route.ts
📚 Learning: when zod enum validation is used (e.g., `z.enum(payment_method_types)`), it ensures the validated va...
Learnt from: devkiran
PR: dubinc/dub#2433
File: apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts:74-76
Timestamp: 2025-05-29T09:47:33.583Z
Learning: When Zod enum validation is used (e.g., `z.enum(PAYMENT_METHOD_TYPES)`), it ensures the validated value is one of the valid enum values, making subsequent type casts to the corresponding TypeScript types safe without additional validation.
Applied to files:
apps/web/app/api/workspaces/[idOrSlug]/billing/invoices/route.ts
📚 Learning: in apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition ob...
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/api/workspaces/[idOrSlug]/billing/invoices/route.ts
apps/web/ui/domains/add-edit-domain-form.tsx
📚 Learning: in apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined res...
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
packages/email/src/templates/domain-renewal-failed.tsx
packages/email/src/templates/domain-renewed.tsx
📚 Learning: in apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calcul...
Learnt from: TWilson023
PR: dubinc/dub#2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation `3 - i * -0.15` is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.
Applied to files:
apps/web/ui/modals/domain-auto-renewal-modal.tsx
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx
📚 Learning: in the dub codebase, components that use workspace data (workspaceid, defaultprogramid) are wrapped ...
Learnt from: TWilson023
PR: dubinc/dub#2538
File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27
Timestamp: 2025-06-18T20:26:25.177Z
Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.
Applied to files:
apps/web/ui/domains/add-edit-domain-form.tsx
📚 Learning: in the dub codebase, it's acceptable to keep `partners.dub.co` hardcoded rather than making it confi...
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:254-254
Timestamp: 2025-05-29T04:49:42.842Z
Learning: In the Dub codebase, it's acceptable to keep `partners.dub.co` hardcoded rather than making it configurable for different environments.
Applied to files:
apps/web/ui/domains/domain-card.tsx
🧬 Code Graph Analysis (7)
apps/web/app/(ee)/api/stripe/webhook/route.ts (1)
apps/web/app/(ee)/api/stripe/webhook/charge-refunded.ts (1)
chargeRefunded
(6-33)
apps/web/lib/dynadot/set-renew-option.ts (1)
packages/utils/src/functions/log.ts (1)
log
(9-45)
packages/email/src/templates/domain-renewal-reminder.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter
(1-11)
apps/web/lib/stripe/create-payment-intent.ts (3)
apps/web/lib/stripe/index.ts (1)
stripe
(3-9)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter
(1-11)packages/utils/src/functions/log.ts (1)
log
(9-45)
apps/web/app/api/domains/[domain]/route.ts (3)
apps/web/lib/zod/schemas/domains.ts (1)
updateDomainBodySchema
(166-166)apps/web/lib/api/errors.ts (1)
DubApiError
(75-92)apps/web/lib/dynadot/set-renew-option.ts (1)
setRenewOption
(12-61)
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/partner-payout-invoice.tsx (3)
apps/web/lib/stripe/index.ts (1)
stripe
(3-9)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter
(1-11)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL
(29-29)
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (2)
apps/web/lib/dynadot/set-renew-option.ts (1)
setRenewOption
(12-61)packages/email/src/templates/domain-renewed.tsx (1)
DomainRenewed
(19-119)
🔇 Additional comments (23)
apps/web/app/(ee)/api/stripe/webhook/route.ts (3)
6-6
: LGTM: Import follows established pattern.The import of
chargeRefunded
handler is consistent with other webhook handler imports in this file.
16-16
: LGTM: Event type correctly added to relevant events.Adding "charge.refunded" to the
relevantEvents
set follows the established pattern for webhook event filtering.
54-56
: LGTM: Switch case implementation is correct.The new case for "charge.refunded" follows the same pattern as other webhook event handlers, properly calling the
chargeRefunded
function with the event parameter.apps/web/vercel.json (1)
7-14
: LGTM: Cron job configurations are correct.The new cron jobs for domain renewal reminders and payments are properly configured with valid cron syntax. Both jobs are scheduled to run daily at 8:00 AM UTC, which appears intentional for coordinated domain renewal processing.
The paths follow the established pattern and align with the new API endpoints introduced in this PR.
apps/web/lib/types.ts (1)
235-238
: LGTM: Type additions support domain renewal functionality.The new
autoRenewalDisabledAt
andrenewalFee
properties are well-designed:
autoRenewalDisabledAt
as nullable Date correctly represents the auto-renewal state (null = enabled, date = when disabled)renewalFee
as number properly types the renewal costThese additions align with the corresponding schema updates and support the domain renewal features introduced in this PR.
apps/web/app/(ee)/api/admin/payouts/route.ts (2)
45-56
: LGTM: Improved filtering logic prevents null reference errors.The updated filtering logic is a good defensive programming improvement:
- The AND condition ensures both
programId
is notACME_PROGRAM_ID
ANDprogram
is not null- This prevents potential null reference errors when accessing
program.name
andprogram.logo
The approach is safer than the previous single condition filter.
102-104
: LGTM: Non-null assertions are now safe.The non-null assertions for
program.name
andprogram.logo
are justified by the filtering logic above that ensuresprogram
is not null. The explanatory comment makes the reasoning clear.apps/web/lib/zod/schemas/domains.ts (1)
12-18
: LGTM: Well-structured schema additions for domain renewal.The new fields are properly defined:
autoRenewalDisabledAt
as nullable date correctly models the auto-renewal staterenewalFee
as number appropriately types the renewal cost- Both fields have clear, descriptive documentation
These schema additions align perfectly with the corresponding TypeScript interface changes and support the domain renewal functionality.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx (1)
45-46
: LGTM! Invoice type tabs align with backend changes.The addition of the "Domain Renewals" tab and the renaming of the partner payout ID from
"payout"
to"partnerPayout"
correctly reflects the expanded invoice types supported by the system.apps/web/app/(ee)/api/cron/invoices/retry-failed/route.ts (1)
13-81
: Well-structured retry logic with proper safeguardsThe implementation includes all necessary validation checks:
- Request signature verification via Qstash
- Invoice existence and status validation
- Retry attempt limits (max 3)
- Invoice type restriction to domain renewals only
- Stripe ID requirement
- Idempotency key to prevent duplicate charges
packages/prisma/schema/invoice.prisma (2)
7-10
: LGTM! Well-structured enum for invoice types.The
InvoiceType
enum cleanly separates partner payouts from domain renewals, enabling proper handling of different invoice workflows.
14-14
: LGTM! Schema changes support domain renewal invoices well.The changes are well-designed to support the new domain renewal functionality:
- Making
programId
nullable allows domain renewal invoices without programs- The
type
field with defaultpartnerPayout
ensures backward compatibilityregisteredDomains
JSON field provides flexibility for storing domain datafailedAttempts
field enables retry logic for failed payments- Nullable
program
relation aligns with the nullableprogramId
These changes maintain backward compatibility while enabling the new domain renewal features.
Also applies to: 18-18, 24-26, 31-31
packages/email/src/templates/domain-renewal-failed.tsx (1)
20-44
: LGTM! Well-structured email template.The email template has a good structure with proper default props, appropriate pluralization handling, and clean React Email component usage.
apps/web/app/api/domains/[domain]/route.ts (2)
22-24
: LGTM! Clean schema extension.The extended schema properly adds the optional
autoRenew
field while maintaining backward compatibility with the existingupdateDomainBodySchema
.
151-176
: LGTM! Well-implemented auto-renewal sync logic.The auto-renewal synchronization is well-designed:
- Only processes when domain is registered and
autoRenew
is provided- Correctly determines when an update is needed to avoid unnecessary operations
- Properly updates
autoRenewalDisabledAt
field (null for enabled, current date for disabled)- Uses
waitUntil
to asynchronously sync with Dynadot API without blocking the responseThe logic is sound and follows good practices for external API integration.
packages/email/src/templates/domain-renewed.tsx (1)
19-42
: LGTM! Clean email template structure.The template has a well-designed structure with proper props, pluralization handling, and clear messaging for successful domain renewals.
apps/web/app/(ee)/api/cron/domains/renewal-reminders/route.ts (1)
18-30
: LGTM! Well-documented cron job with clear reminder windows.The cron job is well-structured with clear documentation of the reminder intervals and proper configuration.
apps/web/ui/domains/add-edit-domain-form.tsx (1)
65-67
: LGTM! Well-structured auto-renewal feature implementation.The auto-renewal functionality is cleanly integrated into the existing form structure. The conditional rendering ensures it only appears for Dub-provisioned domains, and the state management correctly reflects the current auto-renewal status.
Also applies to: 130-138, 154-156, 380-491, 619-624
apps/web/app/api/workspaces/[idOrSlug]/billing/invoices/route.ts (1)
10-14
: LGTM! Clean refactoring to support multiple invoice types.The consolidation of invoice handling into a generic
otherInvoices
function is well-implemented. The type safety through Zod validation ensures only valid invoice types are processed.Also applies to: 24-30, 56-88
apps/web/ui/domains/domain-card.tsx (4)
104-119
: LGTM! Clean implementation of expiration and auto-renewal state management.The logic correctly derives the auto-renewal status from the
autoRenewalDisabledAt
field and properly checks for expired domains usingisPast
.
139-164
: Well-structured renewal status UI with clear visual indicators.The implementation provides excellent user feedback through color-coded states and appropriate icons. The conditional text clearly communicates whether the domain will auto-renew or expire.
334-339
: Good refactoring: Hooks properly organized at the component top.Following React best practices by placing all hooks at the beginning of the component improves code maintainability and ensures compliance with the Rules of Hooks.
495-515
: Clean implementation of the auto-renewal toggle with intuitive UX.The toggle button provides clear action labels and appropriate color coding (green for enable, red for disable) that enhances user understanding of the current state and available action.
apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/domain-renewal-invoice.tsx
Outdated
Show resolved
Hide resolved
@coderabbitai full review |
✅ Actions performedFull review triggered. |
@coderabbitai I made a few final changes, can you do a full review please 🙏 |
@steven-tey I'll conduct a comprehensive full review of your final changes to the .link domain renewals implementation. Thank you for the iterative improvements! ✅ Actions performedFull review triggered. |
@coderabbitai full review please |
@steven-tey I'll conduct a comprehensive full review of your .link domain renewals implementation. This looks like a well-structured feature with cron jobs, Stripe integration, email notifications, and UI components for auto-renewal management. ✅ Actions performedFull review triggered. |
@coderabbitai there was no review, can you do a full review please 🙏 |
@steven-tey I apologize for the issue with the previous review attempts. I'll trigger a comprehensive full review of your .link domain renewals implementation right now. This will cover all the components including the cron jobs, Stripe integration, email notifications, database changes, and UI components for auto-renewal management. ✅ Actions performedFull review triggered. |
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: 2
♻️ Duplicate comments (1)
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts (1)
161-161
: Use consistent amount source for payout emailsThe email uses
charge.amount
instead ofinvoice.amount
. This could lead to inconsistencies if there are currency conversions or processing fees.- amount: charge.amount, + amount: invoice.amount,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/success/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx
(3 hunks)apps/web/lib/stripe/create-payment-intent.ts
(1 hunks)apps/web/lib/types.ts
(1 hunks)apps/web/ui/modals/domain-auto-renewal-modal.tsx
(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/success/page-client.tsx
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/web/lib/types.ts
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/invoices/page-client.tsx
- apps/web/lib/stripe/create-payment-intent.ts
- apps/web/ui/modals/domain-auto-renewal-modal.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: paypal webhook verification in the dub codebase is handled at the route level in `apps/web/app/(ee)/...
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-19T01:46:45.723Z
Learning: PayPal webhook verification in the Dub codebase is handled at the route level in `apps/web/app/(ee)/api/paypal/webhook/route.ts` using the `verifySignature` function. Individual webhook handlers like `payoutsItemFailed` don't need to re-verify signatures since they're only called after successful verification.
Applied to files:
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
📚 Learning: the `/api/workspaces/${slug}/billing/payment-methods` post endpoint in the billing api returns eithe...
Learnt from: devkiran
PR: dubinc/dub#2433
File: apps/web/ui/modals/add-payment-method-modal.tsx:60-62
Timestamp: 2025-05-29T09:49:19.604Z
Learning: The `/api/workspaces/${slug}/billing/payment-methods` POST endpoint in the billing API returns either an error (handled by response.ok check) or a response object containing a `url` property for successful requests.
Applied to files:
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts
🧬 Code Graph Analysis (1)
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts (6)
packages/utils/src/functions/log.ts (1)
log
(9-45)apps/web/lib/stripe/create-payment-intent.ts (1)
createPaymentIntent
(4-76)apps/web/lib/partners/constants.ts (1)
PAYOUT_FAILURE_FEE_CENTS
(7-7)apps/web/lib/dynadot/set-renew-option.ts (1)
setRenewOption
(12-61)packages/email/src/templates/domain-expired.tsx (1)
DomainExpired
(19-124)packages/email/src/templates/domain-renewal-failed.tsx (1)
DomainRenewalFailed
(20-124)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts (3)
1-18
: LGTM on the new imports.The additional imports properly support the new domain renewal functionality and follow the established patterns in the codebase.
20-59
: Excellent refactor improving code organization.The separation of concerns between partner payouts and domain renewals makes the code more maintainable. The early return pattern and type-based delegation are well implemented.
192-192
: Confirm registeredDomains nullability and usageBefore trusting the
as string[]
assertion, please verify howregisteredDomains
is defined in your Prisma schema and how it’s populated:
- Check the Invoice model in your Prisma schema: is
registeredDomains
declared as a non-nullableString[]
?- If it’s non-nullable: you can remove the redundant
as string[]
cast – Prisma will already type it asstring[]
.- If it’s optional (
String[]?
): add a runtime guard (e.g. default to[]
or throw) to avoid unexpectednull
/undefined
at runtime.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores