Repository navigation
Description
Needs your OK: this adds an optional Email box to the "Your spot" page for someone who signed up without one, and emails them their link once they fill it in. Swap pipeline:proposed for pipeline:ready to have it built.
Follow-up to #347.
What happens
Since #378 an organizer can make the email optional. Someone who leaves it blank gets no emails at all. Their only ways back to their spot are the link shown after signing up and the os_commit cookie in the browser they used.
They can't add an email later:
- The page. "Your spot" (
src/app/s/[slug]/c/[id]/page.tsxandedit-form.tsx) changes name, notes and spots, or cancels. Without an email it reads "You're editing this as {name}." and offers nothing more. - The API.
PATCH /api/commitments/[id]takesname,notes,quantityandswapToSlotId(CommitmentUpdateInputSchemainsrc/schemas/commitments.ts), nothing else. - The workaround. Sign up again with an email, then cancel the old spot. If the slot has filled meanwhile, they can't.
These are the people most likely to lose their link, and the ones a reminder would help most. #657 (proposed) would email a lost link to an address already on the signup, so it can't help someone who never gave one.
#378 left this out on purpose, because it is a new way to make the app send mail. The fix below meters it exactly as signing up is metered.
Fix, in outline
- The box. On "Your spot", only when the participant has no email: "Email (optional)", starting blank, with the line "Add your email and we'll send you this link, and a reminder before your slot." Show the reminder part only where
willSendRemindersays the slot gets one, as the sign-up sheet does.editLimitsForCommitmentalready reads the slot's time and the signup's settings. Show the sheet's "Did you mean …?" hint (suggestEmail) under the box. - The request.
CommitmentUpdateInputSchemagains an optionalemail, with the same rules as signing up. Blank or missing means no change. Anemailsent withswapToSlotIdis refused asinvalid_input: the page never sends both. - The service. In the same-slot path of
updateOwnCommitment, after the commitment lock, lock the participant (for no key update; participant last, as everywhere else). Then:- Already has an email: refuse with
invalid_inputonemail. This adds an email; it never changes one. - Another participant on this signup has that address: refuse with
conflictonemail, and the message "Someone on this signup already uses that email. If it's you, use the link we sent to it." - Otherwise: write both columns through
participantEmailColumns, and recordparticipant.email_addedwithrecordActivityin the same transaction, payload{ participantId, commitmentId }and no address. - A race on the address. A sign-up or another add with the same address can still hit the
participants_signup_emailunique index between the check and the write. Catch that one violation and return the sameconflict, not a 500.
- Already has an email: refuse with
- The email. Have the service report that it added an email. The PATCH route then calls
notifyCommitmentCreated(db, id, token, { meterRecipient: true })fromafter(), as a move does. No confirmation was ever sent for this commitment, so its once-only check lets this one through.meterRecipientcharges the new address againstcommitmentPerEmail, as signing up does, and the request has already passedcommitmentTokenOpsPerIp. - After the save. The page refreshes and reads "You're editing this as {name} ({address})." The saved message says "Saved. We're emailing your link to {address}."
- Reminders and moved-slot emails need no change.
notifiableRulesinsrc/jobs/notify-rules.tsskips only a participant with no email.
Watch for
- A "time changed" email on top of the confirmation.
toldDifferentTimeinsrc/jobs/notify-rules.tscounts from the commitment's creation when nobody was ever emailed. If the slot moved after they signed up, the slot-change scan (every 2 minutes) can pick them up once the address is saved. The confirmation recordscommitment.confirmation_sent, which should settle it. Pin it with a DB test: a slot moved before the email was added gets the confirmation and no change email. - The trust docs. The privacy page (
src/app/(legal)/privacy/page.tsx) says someone who leaves the email blank gets no emails. Add that they can add one later from their link, and bump "Last updated". The "four kinds of email" count doesn't change: this sends the existing confirmation. - Help. "Let people sign up without an email" in
src/help/articles/create-and-publish-a-signup.tsxshould say people can add an email later from their link. Run the help-pages skill'sscripts/affected-articles.mjs. - The page budget.
/s/[slug]/c/[id]is capped at 110 kB of first-load script.suggestEmailis small, but checkpnpm budgets. - Nearby work. On the edit page, spaces after a name count toward the 100-character limit #723 (ready) changes name handling on the same page. Open PR Centralize participant input limits in one module #706 edits
edit-form.test.tsx,participant-fields.tsxandsrc/schemas/commitments.ts. The "You're in." confirmation doesn't say which email address it used #715 (ready) names the address on the "You're in." sheet and says the edit page can't change an email; that stays true, since this only adds one. Whichever lands second has a small merge. - Telemetry. Add
participant.email_addedtodocs/telemetry.md. - CHANGELOG line.
Choices this makes
- Add only, never change. Default: the box shows only without an email, and the service refuses to replace one. Changing an address raises more questions, and nobody has asked for it.
- Anyone with the link can add one. Default: yes. The link can already cancel the spot, so adding an address gives its holder nothing new.
- Refuse an address already on this signup, don't merge. Default: refuse, with the message above. It tells the holder of an edit link that the address is on the signup; the sign-up sheet already says as much for a slot that address holds. Merging two participants means moving sign-ups between them, which isn't worth it here.
- No confirming the address first. Default: the confirmation email is the check. If it doesn't arrive, they still have their link, and the "Did you mean" hint catches common typos.
- A reminder may follow within minutes. Default: accept. If the slot is within a day, the next reminder scan (every 10 minutes) sends one soon after the confirmation, because
REMINDER_SETTLE_HOURScounts from when they signed up. - Words. Default: the label, the line under the box and the saved message above.
Done when
- On a signup with the email optional, someone who signed up without one opens their link, adds an address, and gets the confirmation. Reminders and moved-slot emails then reach them.
- DB tests in
src/services/commitments.db.test.tsandsrc/app/api/commitments/[id]/route.db.test.ts:- the address is stored in both columns, with the activity row in the same transaction and no address in it
- refused when the participant already has an email, and when another participant on the signup uses it, including the unique-index race, which must not be a 500
- the confirmation is sent once and charged to the address
- a slot moved before the add gets the confirmation and no change email
edit-form.test.tsx: the box shows only without an email, and the request carries it.- The privacy and help pages say it, and the privacy page's date is bumped.
pnpm lint && pnpm typecheck && pnpm knip && pnpm test,pnpm test:db,pnpm build && pnpm budgetsandtests/e2e/edit-token.spec.tspass.
Rewritten from the original issue for the pipeline, and checked against src/app/s/[slug]/c/[id]/page.tsx, edit-form.tsx, src/app/api/commitments/[id]/route.ts, src/schemas/commitments.ts, src/services/commitments.ts, src/email/notify.ts, src/jobs/notify-rules.ts and src/app/(legal)/privacy/page.tsx on main at 2d4fdfe. https://claude.ai/code/session_016YZ9SUWWJa1udHCjU5fUma
Activity
[-]Let someone who signed up without an email add one later[/-][+]Someone who signed up without an email can't add one later[/+]