Skip to content

Use SDK 0.18 and add react-emails and resend intrgration#87

Merged
damianlegawiec merged 4 commits into
mainfrom
feature/react-emails
Mar 26, 2026
Merged

Use SDK 0.18 and add react-emails and resend intrgration#87
damianlegawiec merged 4 commits into
mainfrom
feature/react-emails

Conversation

@damianlegawiec
Copy link
Copy Markdown
Member

@damianlegawiec damianlegawiec commented Mar 26, 2026

Summary by CodeRabbit

  • New Features

    • Added transactional emails: order confirmation, order cancellation, shipment notification, and password reset (with preview support).
  • Documentation

    • Added setup and usage docs for transactional emails, webhook integration, and local preview workflow.
  • Chores

    • Added email preview/dev script and installed libraries for rendering and sending emails.
  • Bug Fixes

    • Middleware routing updated to exclude API endpoints from proxy handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7190af44-1ebc-4d83-8772-2f8d1802b2fa

📥 Commits

Reviewing files that changed from the base of the PR and between d65fc2f and 2fbc44b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • src/app/api/webhooks/spree/route.ts
  • src/lib/emails/order-canceled.tsx
  • src/lib/emails/order-confirmation.tsx
  • src/lib/emails/password-reset.tsx
  • src/lib/emails/shipment-shipped.tsx
  • src/lib/webhooks/handlers.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/emails/order-confirmation.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/api/webhooks/spree/route.ts
  • src/lib/emails/password-reset.tsx
  • src/lib/emails/shipment-shipped.tsx

Walkthrough

Adds 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

Cohort / File(s) Summary
Config & Docs
\.env\.example, README\.md, package\.json
Added SPREE_WEBHOOK_SECRET, RESEND_API_KEY, EMAIL_FROM env vars and docs for transactional emails; added email-related deps (resend, @react-email/components, react-email) and bumped @spree/next / @spree/sdk to ^0.18.0; added email:dev script.
Email Templates
src/lib/emails/order-confirmation.tsx, src/lib/emails/order-canceled.tsx, src/lib/emails/shipment-shipped.tsx, src/lib/emails/password-reset.tsx
New React Email templates exporting typed components that render HTML emails (items, totals, addresses, tracking, reset CTA) with local styling constants.
Email Previews
emails/order-confirmation.tsx, emails/order-canceled.tsx, emails/shipment-shipped.tsx, emails/password-reset.tsx
Added preview pages/components with hard-coded sample data for each template to facilitate local development.
Send Utility
src/lib/emails/send.ts
Added sendEmail that either writes rendered HTML to .next/emails/ in dev or sends via Resend in production (uses RESEND_API_KEY / EMAIL_FROM).
Webhooks & Handlers
src/app/api/webhooks/spree/route.ts, src/lib/webhooks/handlers.ts
New POST webhook route verifying SPREE_WEBHOOK_SECRET and dispatching to handlers that map Spree events (order.completed, order.canceled, order.shipped, customer.password_reset_requested) to email sends with an in-memory idempotency set.
Category & UI adjustments
src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx, .../page.tsx, src/components/ui/dropdown-menu.tsx
Removed categoryPermalink prop and switched data fetches to categoryId; updated call sites; reordered dropdown-menu exports and adjusted Content className (minor CSS token change).
Middleware matcher
src/proxy.ts
Excluded /api/ routes from middleware matcher to prevent proxy running on API endpoints.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Bump SDK to 0.16 #83: Dependency bump of @spree/next and @spree/sdk to ^0.18.0, same version updates as this PR.
  • Use Categories #51: Changes to CategoryProductsContent props/fetching (removal of categoryPermalink) that directly relate to the category refactor here.
  • Use Shadcn #50: Modifications to src/components/ui/dropdown-menu.tsx overlapping export/order and className adjustments.

Poem

🐰 Hopping through webhooks, I nab a new thread,

Orders and resets — tidy emails are spread.
I stitch React to HTML, then off they are sent,
Dev files for testing, production well-meant.
Cheers, little rabbit, your inbox is fed!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions SDK 0.18 upgrade and react-emails/resend integration, which are core changes in this PR, but contains a typo ('intrgration' instead of 'integration') and is somewhat vague about the specific scope.

✏️ 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 feature/react-emails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 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 sendEmailDev and sendEmailResend.

🤖 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/components library 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.

displayDiscountTotal and displayTaxTotal are already presentation strings. Re-parsing them here is locale-sensitive, and NaN !== 0 will still render the discount row for any non-numeric label. Pass raw numeric totals or boolean flags from src/lib/webhooks/handlers.ts and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 94773da and d65fc2f.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .env.example
  • README.md
  • emails/order-canceled.tsx
  • emails/order-confirmation.tsx
  • emails/password-reset.tsx
  • emails/shipment-shipped.tsx
  • package.json
  • src/app/[country]/[locale]/(storefront)/c/[...permalink]/CategoryProductsContent.tsx
  • src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx
  • src/app/api/webhooks/spree/route.ts
  • src/components/ui/dropdown-menu.tsx
  • src/lib/emails/order-canceled.tsx
  • src/lib/emails/order-confirmation.tsx
  • src/lib/emails/password-reset.tsx
  • src/lib/emails/send.ts
  • src/lib/emails/shipment-shipped.tsx
  • src/lib/webhooks/handlers.ts
  • src/proxy.ts
💤 Files with no reviewable changes (1)
  • src/app/[country]/[locale]/(storefront)/c/[...permalink]/page.tsx

Comment thread src/app/api/webhooks/spree/route.ts Outdated
Comment on lines +52 to +55
<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>
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

🧩 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:


🏁 Script executed:

# Find the password-reset email file and examine its implementation
find . -name "password-reset*" -type f | head -20

Repository: 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 -20

Repository: 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 3

Repository: 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 -70

Repository: spree/storefront

Length of output: 2208


🏁 Script executed:

# Search for usages of PasswordResetEmail to see how it's called
rg "PasswordResetEmail" -A 5 -B 5

Repository: 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.

Comment thread src/lib/emails/shipment-shipped.tsx Outdated
Comment thread src/lib/webhooks/handlers.ts Outdated
Comment thread src/lib/webhooks/handlers.ts Outdated
Comment thread src/lib/webhooks/handlers.ts
Comment thread src/lib/webhooks/handlers.ts Outdated
@damianlegawiec damianlegawiec merged commit 0ca22c2 into main Mar 26, 2026
4 checks passed
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.

1 participant