Account settings + delete account#104
Conversation
Adds product/marketing email toggles to the account settings page, backed by a new GET-augmented `/api/account` payload and a `PUT /api/account/notifications` endpoint. Marketing opt-out mirrors to the Resend audience so bulk sends honor the preference without re-implementing list segmentation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ 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. ℹ️ 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 (4)
✨ Finishing Touches🧪 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 |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Code Review
This pull request implements per-user notification preferences for product and marketing emails, including a database migration, a new API endpoint, and frontend UI updates. The implementation also mirrors marketing opt-out status to Resend. Feedback was provided to improve data consistency by using the RETURNING clause in the SQL update and to enhance performance by offloading the Resend API call to a background task using waitUntil.
| await c.env.DB.prepare( | ||
| `UPDATE user | ||
| SET notify_product_emails = ?, notify_marketing_emails = ?, updatedAt = ? | ||
| WHERE id = ?`, | ||
| ) | ||
| .bind( | ||
| parsed.data.productEmails ? 1 : 0, | ||
| parsed.data.marketingEmails ? 1 : 0, | ||
| Date.now(), | ||
| user.id, | ||
| ) | ||
| .run(); |
There was a problem hiding this comment.
Using RETURNING email in the UPDATE statement allows you to retrieve the most up-to-date email address directly from the database. This ensures that the subsequent Resend mirroring uses the correct email, even if it was recently changed and the session data in the request context is stale.
| await c.env.DB.prepare( | |
| `UPDATE user | |
| SET notify_product_emails = ?, notify_marketing_emails = ?, updatedAt = ? | |
| WHERE id = ?`, | |
| ) | |
| .bind( | |
| parsed.data.productEmails ? 1 : 0, | |
| parsed.data.marketingEmails ? 1 : 0, | |
| Date.now(), | |
| user.id, | |
| ) | |
| .run(); | |
| const row = await c.env.DB.prepare( | |
| `UPDATE user | |
| SET notify_product_emails = ?, notify_marketing_emails = ?, updatedAt = ? | |
| WHERE id = ? | |
| RETURNING email`, | |
| ) | |
| .bind( | |
| parsed.data.productEmails ? 1 : 0, | |
| parsed.data.marketingEmails ? 1 : 0, | |
| Date.now(), | |
| user.id, | |
| ) | |
| .first<{ email: string }>(); | |
| const email = row?.email ?? user.email; |
| await upsertContact(resendEnv, { | ||
| email: user.email, | ||
| unsubscribed: !parsed.data.marketingEmails, | ||
| }).catch((err) => { | ||
| console.warn('resend marketing-pref mirror failed', { | ||
| userId: user.id, | ||
| error: err instanceof Error ? err.message : String(err), | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Mirroring the preference to Resend is a side effect that doesn't need to block the user's request. Using c.executionCtx.waitUntil() allows the response to be sent immediately while the background task completes, improving the perceived performance of the settings update.
c.executionCtx.waitUntil(
upsertContact(resendEnv, {
email,
unsubscribed: !parsed.data.marketingEmails,
}).catch((err) => {
console.warn('resend marketing-pref mirror failed', {
userId: user.id,
error: err instanceof Error ? err.message : String(err),
});
}),
);There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9872859e8d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await upsertContact(resendEnv, { | ||
| email: user.email, | ||
| unsubscribed: !parsed.data.marketingEmails, | ||
| }).catch((err) => { |
There was a problem hiding this comment.
Check Resend upsert result before treating update as synced
/api/account/notifications awaits upsertContact(...).catch(...), but upsertContact reports HTTP/network failures via { ok: false, ... } return values rather than throwing, so this catch path is usually never hit. In cases like invalid Resend credentials or Resend API errors, the endpoint still returns 200 and the UI shows success while the marketing opt-out is not mirrored, leaving contacts subscribed in Resend despite the saved preference; inspect the returned ResendResult and handle non-ok outcomes explicitly.
Useful? React with 👍 / 👎.
Closes ALO-132.
Email/password change and 30-day grace-period delete already shipped on
main(migrations 0012, account.ts routes, AccountSettings page). This PR adds the remaining piece called out in the ticket: manage notifications.Summary
0019_notification_prefs.sqladdsnotify_product_emailsandnotify_marketing_emailstouser(default 1).GET /api/accountnow returnsnotifications: { productEmails, marketingEmails }.PUT /api/account/notificationsvalidates with zod, persists, and mirrors marketing opt-out to the Resend audience (unsubscribedflag) when Resend is configured.Test plan
npm test— 44 files, 493 tests passingnpm run lint— clean (incl. AI-Gateway guard)npm run type-check— clean