Use SDK 0.18 and add react-emails and resend intrgration#87
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughAdds a transactional email system: React Email templates and previews, a Resend-backed send utility with dev-mode HTML output, Spree webhook route and handlers mapping events to emails, env/config docs, and related dependency and minor UI/category refactors. Changes
Sequence DiagramsequenceDiagram
participant Spree as Spree Platform
participant Webhook as Webhook Route
participant Handler as Webhook Handler
participant EmailTpl as Email Template
participant SendUtil as send.ts
participant Resend as Resend Service
participant FS as File System
Spree->>Webhook: POST /api/webhooks/spree (signed event)
Webhook->>Handler: verify & dispatch (order.completed / canceled / shipped / password_reset_requested)
Handler->>Handler: extract & map order/customer data
Handler->>EmailTpl: construct React email element (OrderConfirmation/ShipmentShipped/...)
Handler->>SendUtil: sendEmail({to, subject, react})
alt Dev mode or missing RESEND_API_KEY
SendUtil->>FS: render React -> HTML
FS-->>FS: write timestamped .next/emails/*.html
else Production with RESEND_API_KEY
SendUtil->>Resend: resend.emails.send(...)
Resend-->>SendUtil: response / error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
src/lib/emails/send.ts (1)
15-27: Add explicit return types for async functions.Per coding guidelines, functions should have explicit return types. The async functions here implicitly return
Promise<void>.🔧 Proposed type annotations
-export async function sendEmail({ +export async function sendEmail({ to, subject, react, from, -}: SendEmailOptions) { +}: SendEmailOptions): Promise<void> {Apply similarly to
sendEmailDevandsendEmailResend.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/emails/send.ts` around lines 15 - 27, The async functions lack explicit return types—update the declarations for sendEmail and the helper functions sendEmailDev and sendEmailResend to include explicit Promise<void> return types (e.g., async function sendEmail(...): Promise<void> { ... }) so the intent and typing are clear and consistent with project guidelines; ensure all three function signatures are updated accordingly.src/lib/emails/shipment-shipped.tsx (1)
104-106: Consider email client compatibility for native<div>.Some email clients may not render native
<div>elements consistently. The@react-email/componentslibrary provides table-based layout components that typically have better compatibility. This placeholder may render inconsistently in Outlook and older clients.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/emails/shipment-shipped.tsx` around lines 104 - 106, Replace the plain <div style={imagePlaceholder} /> placeholder with a table-based component from `@react-email/components` to improve email client compatibility: swap the <div> for a <Box style={imagePlaceholder} /> (or another table-based component such as <Section>/<Column> if you prefer) and add the import statement import { Box } from '@react-email/components'; keep the same imagePlaceholder style object and JSX location so the layout/visual stays identical but is rendered using the library's table-based markup instead of a native div.src/lib/emails/order-confirmation.tsx (1)
142-154: Don't parse formatted totals to decide whether rows render.
displayDiscountTotalanddisplayTaxTotalare already presentation strings. Re-parsing them here is locale-sensitive, andNaN !== 0will still render the discount row for any non-numeric label. Pass raw numeric totals or boolean flags fromsrc/lib/webhooks/handlers.tsand keep this template render-only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/emails/order-confirmation.tsx` around lines 142 - 154, The template is re-parsing presentation strings (displayDiscountTotal and displayTaxTotal) to decide rendering; instead, accept raw numeric totals or explicit booleans from the generator/handler and use those in the JSX conditionals. Update src/lib/webhooks/handlers.ts to pass numeric fields like discountTotal (number) and taxTotal (number) or flags like hasDiscount/hasTax into the props used by order-confirmation.tsx, then change the JSX conditions in order-confirmation.tsx to check those numeric/boolean props (e.g., discountTotal !== 0 or hasDiscount, taxTotal > 0 or hasTax) and render displayDiscountTotal/displayTaxTotal only for presentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/api/webhooks/spree/route.ts`:
- Around line 9-10: Remove the non-null assertion and validate
process.env.SPREE_WEBHOOK_SECRET at module initialization: read the variable
into a const (e.g., const spreeSecret = process.env.SPREE_WEBHOOK_SECRET), and
if it's falsy throw a clear Error so startup fails rather than calling
createWebhookHandler with undefined; then pass spreeSecret to
createWebhookHandler for the POST export. This ensures createWebhookHandler
(used in POST) always receives a real secret and prevents disabled signature
verification.
In `@src/lib/emails/password-reset.tsx`:
- Around line 52-55: The password reset email currently hardcodes "15 minutes"
in the Text styled by disclaimer in src/lib/emails/password-reset.tsx; update
PasswordResetEmailProps to accept a ttl (e.g., string or number) and use that
prop in the displayed copy instead of the literal "15 minutes", then ensure the
caller in src/lib/webhooks/handlers.ts passes the configured
customer_password_reset_expires_in value when constructing the
PasswordResetEmail; alternatively, if you prefer not to pass a TTL, change the
copy to a generic phrase like "This link will expire shortly" to avoid stale
text.
In `@src/lib/emails/shipment-shipped.tsx`:
- Around line 93-95: The map rendering in shipment.items.map uses item.name as
the React key which can collide; update the key on the Row (and any sibling
mapped elements) to use a stable unique identifier such as item.id, item.sku, or
a composite like `${item.id}-${item.variantId}` and fall back to the map index
only if no stable id exists (e.g., use item.id ?? `${item.name}-${index}`),
ensuring the key is unique and stable across renders for components like Row and
Column.
In `@src/lib/webhooks/handlers.ts`:
- Around line 26-52: Webhook handlers are currently calling sendEmail directly
(e.g., the block that builds the react OrderConfirmationEmail with createElement
and calls sendEmail), which is non-idempotent; persist and check a unique
webhook/event key (e.g., using the incoming webhook ID or event ID) in a durable
store before invoking sendEmail, return early if the key already exists, and
only call sendEmail after atomically marking the event as processed; apply the
same pattern to the other email send sites referenced (lines ~65-83, ~130-140,
~174-182) to ensure duplicate webhook deliveries don’t produce duplicate emails.
- Around line 10-11: The STORE_URL constant currently falls back to
"http://localhost:3001" which causes outbound emails to contain a dev origin;
update the initialization of STORE_URL in handlers.ts so it only defaults to the
localhost URL in development (e.g. when process.env.NODE_ENV === "development")
and otherwise fails fast when missing (throw an error or log and exit) so
production/preview builds cannot silently use a dev origin; update any code that
reads STORE_URL (the STORE_URL constant) and ensure tests or dev scripts set the
env when relying on the dev fallback.
- Around line 165-172: The fallback branch builds resetUrl by concatenating
STORE_URL and reset_token which can break with reserved characters and relative
paths; update both branches to construct URLs via the URL constructor: for the
provided redirect_url use new URL(redirect_url, STORE_URL) and for the fallback
use new URL('/account/reset-password', STORE_URL), then call
url.searchParams.set('token', reset_token) and assign resetUrl = url.toString();
keep the existing resetUrl variable and ensure redirect_url handling remains
guarded (i.e., only use new URL(redirect_url, STORE_URL) when redirect_url is
truthy).
- Around line 23-24: The current single-value fallback collapses
multi-fulfillment orders by picking order.fulfillments?.[0], which can misreport
the shipping method; instead compute the set of distinct delivery method names
from order.fulfillments (map each fulfillment to
fulfillment.delivery_method?.name, filter out falsy values, dedupe) and then set
deliveryMethodName to the single name if the deduped array length is 1,
otherwise set deliveryMethodName to undefined (or optionally join the distinct
names if you prefer to display multiple methods); update usage of
deliveryMethodName in handlers.ts accordingly.
---
Nitpick comments:
In `@src/lib/emails/order-confirmation.tsx`:
- Around line 142-154: The template is re-parsing presentation strings
(displayDiscountTotal and displayTaxTotal) to decide rendering; instead, accept
raw numeric totals or explicit booleans from the generator/handler and use those
in the JSX conditionals. Update src/lib/webhooks/handlers.ts to pass numeric
fields like discountTotal (number) and taxTotal (number) or flags like
hasDiscount/hasTax into the props used by order-confirmation.tsx, then change
the JSX conditions in order-confirmation.tsx to check those numeric/boolean
props (e.g., discountTotal !== 0 or hasDiscount, taxTotal > 0 or hasTax) and
render displayDiscountTotal/displayTaxTotal only for presentation.
In `@src/lib/emails/send.ts`:
- Around line 15-27: The async functions lack explicit return types—update the
declarations for sendEmail and the helper functions sendEmailDev and
sendEmailResend to include explicit Promise<void> return types (e.g., async
function sendEmail(...): Promise<void> { ... }) so the intent and typing are
clear and consistent with project guidelines; ensure all three function
signatures are updated accordingly.
In `@src/lib/emails/shipment-shipped.tsx`:
- Around line 104-106: Replace the plain <div style={imagePlaceholder} />
placeholder with a table-based component from `@react-email/components` to improve
email client compatibility: swap the <div> for a <Box style={imagePlaceholder}
/> (or another table-based component such as <Section>/<Column> if you prefer)
and add the import statement import { Box } from '@react-email/components'; keep
the same imagePlaceholder style object and JSX location so the layout/visual
stays identical but is rendered using the library's table-based markup instead
of a native div.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f93f0e15-04e5-4602-96b1-55804050d517
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
.env.exampleREADME.mdemails/order-canceled.tsxemails/order-confirmation.tsxemails/password-reset.tsxemails/shipment-shipped.tsxpackage.jsonsrc/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsxsrc/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsxsrc/app/api/webhooks/spree/route.tssrc/components/ui/dropdown-menu.tsxsrc/lib/emails/order-canceled.tsxsrc/lib/emails/order-confirmation.tsxsrc/lib/emails/password-reset.tsxsrc/lib/emails/send.tssrc/lib/emails/shipment-shipped.tsxsrc/lib/webhooks/handlers.tssrc/proxy.ts
💤 Files with no reviewable changes (1)
- src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx
| <Text style={disclaimer}> | ||
| This link will expire in 15 minutes. If you didn't request a | ||
| password reset, you can safely ignore this email. | ||
| </Text> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the default expiration time for password reset tokens in Spree, and is that value configurable?
💡 Result:
The default expiration time for password reset tokens in Spree is 15 minutes. This value is configurable via Spree::Config.customer_password_reset_expires_in.
Citations:
- 1: https://spreecommerce.org/docs/developer/core-concepts/customers
- 2: https://spreecommerce.org/docs/developer/customization/configuration
🏁 Script executed:
# Find the password-reset email file and examine its implementation
find . -name "password-reset*" -type f | head -20Repository: spree/storefront
Length of output: 123
🏁 Script executed:
# Check if there are other email templates to see the pattern for accessing config
fd -t f "email|mail" --extension tsx --extension ts | grep -i email | head -20Repository: spree/storefront
Length of output: 42
🏁 Script executed:
# Search for how Spree::Config is used in email templates or components
rg "Spree::Config|customer_password_reset" -t tsx -t ts -A 3 -B 3Repository: spree/storefront
Length of output: 87
🏁 Script executed:
# Read the password-reset email component to see its structure
cat -n src/lib/emails/password-reset.tsx | head -70Repository: spree/storefront
Length of output: 2208
🏁 Script executed:
# Search for usages of PasswordResetEmail to see how it's called
rg "PasswordResetEmail" -A 5 -B 5Repository: spree/storefront
Length of output: 3195
🏁 Script executed:
# Check other email templates in the same directory for comparison
ls -la src/lib/emails/Repository: spree/storefront
Length of output: 506
Pass the password-reset expiration time as a prop instead of hardcoding it.
The template hardcodes "15 minutes", but Spree's customer_password_reset_expires_in config is changeable. When it changes, the email becomes inaccurate. Add the TTL value to the PasswordResetEmailProps interface and pass it from src/lib/webhooks/handlers.ts where the email is sent, or use generic copy like "This link will expire shortly" to avoid the brittleness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/emails/password-reset.tsx` around lines 52 - 55, The password reset
email currently hardcodes "15 minutes" in the Text styled by disclaimer in
src/lib/emails/password-reset.tsx; update PasswordResetEmailProps to accept a
ttl (e.g., string or number) and use that prop in the displayed copy instead of
the literal "15 minutes", then ensure the caller in src/lib/webhooks/handlers.ts
passes the configured customer_password_reset_expires_in value when constructing
the PasswordResetEmail; alternatively, if you prefer not to pass a TTL, change
the copy to a generic phrase like "This link will expire shortly" to avoid stale
text.
Summary by CodeRabbit
New Features
Documentation
Chores
Bug Fixes