Simplify registration flow: mandatory account, deferred payment, email reminders #6

Closed
opened 2026-03-11 09:51:14 +00:00 by zias · 1 comment
Owner

Context

EventRegistrationForm.tsx and the surrounding registration components are currently too busy. The form tries to handle authentication nudges, optional account creation (dismissible modal), and a Mollie checkout redirect all in one shot, with varying behavior depending on whether the user is logged in. This creates confusion and edge cases.

This issue specifies a simpler, linear flow and three new transactional emails.


New User Flow

Step 1 — Choose role

User sees the existing TypeSelector (two cards):

  • Artiest — "Ik wil optreden"
  • Bezoeker — "Ik wil komen kijken"

No sign-in / sign-up prompt at this stage. Remove the "Al een account?" nudge banner from EventRegistrationForm.tsx.

Step 2 — Fill in info

The regular PerformerForm / WatcherForm fields (name, email, phone, etc.) exactly as today. No changes to the field set itself.

Remove the isLoggedIn / prefill logic from the forms — no more pre-filling from session and no read-only email field for logged-in users. (The user fills in their own info; the account they create in step 3 will be linked by email.)

Step 3 — Bevestigen → mandatory account creation

After the user taps Bevestigen:

  1. submitRegistration is called as today (creates the DB row, sends confirmation email — see email section below).
  2. Instead of showing the dismissible AccountModal or redirecting straight to Mollie, always redirect the user to /login?signup=1&email=<encoded-email>&next=/account (or a dedicated /register page if preferred). Account creation is not optional.
  3. After sign-up the user lands on /account.

Performers do not need to pay, so for them /account is the final destination.
Watchers still owe payment (see step 4).

Step 4 — Payment (Bezoeker only)

On /account, if registration.paymentStatus === "pending" and registration.registrationType === "watcher", show a prominent "Betaal nu" CTA that calls getCheckoutUrl and redirects to Mollie. This replaces the current direct post-form Mollie redirect.

The user can close the browser and pay later — the outstanding payment is shown persistently on /account until resolved.

The existing payment_status column and Mollie webhook handler remain unchanged.


What needs to change

apps/web/src/components/homepage/EventRegistrationForm.tsx

  • Remove the "Al een account?" login nudge banner (lines 117–143).
  • Remove the authClient.useSession() call, the isLoggedIn / prefill logic (lines 48–71).
  • After setSuccessState / onSuccess, do not render SuccessScreen. Instead, redirect to /login?signup=1&email=<email>&next=/account.
  • The successState interface and SuccessScreen import can be removed from this file (SuccessScreen may still be used elsewhere, keep the component itself).

apps/web/src/components/registration/WatcherForm.tsx

  • Remove the AccountModal interstitial that currently appears after successful registration.
  • Remove the direct Mollie getCheckoutUrl call + window.location redirect.
  • On success, call onSuccess(token, email, name) (same as PerformerForm) and let the parent handle the redirect.
  • Remove the isLoggedIn / prefill props (prefillFirstName, prefillLastName, prefillEmail, isLoggedIn); the form is always unauthenticated at this point.

apps/web/src/components/registration/PerformerForm.tsx

  • Remove prefillFirstName, prefillLastName, prefillEmail, isLoggedIn props.
  • The submit flow is unchanged (calls onSuccess), but the parent now handles the account redirect.

apps/web/src/routes/account.tsx

  • If registration.paymentStatus === "pending" and registration.registrationType === "watcher", render a "Betaal je inschrijving" card (or banner) with:
    • Summary: drinkkaart amount + any gift.
    • A "Betaal nu" button that calls orpc.getCheckoutUrl({ token: registration.managementToken }) and redirects to the returned checkoutUrl.
  • The existing claimRegistrationCredit() call on mount covers the case where the user already paid before creating an account — keep that.

apps/web/src/components/registration/SuccessScreen.tsx

  • This component can be deleted unless it is used on the /manage/<token> page. Check usages first; if only used from EventRegistrationForm, remove it.

New Email Triggers

Three new transactional emails need to be added to packages/api/src/email.ts and called from the appropriate places.

1. Signup confirmation (on submit)

Trigger: submitRegistration handler in packages/api/src/routers/index.ts — already calls sendConfirmationEmail. No new email needed here — this is the existing confirmation email. However, update the template to:

  • Not mention a payment link / checkout (that happens via the account page now).
  • Add a line: "Maak je account aan via [link] om je inschrijving te beheren en je drinkkaart te activeren."

2. Payment reminder after 3 days (Bezoeker only, unpaid)

New function: sendPaymentReminderEmail
Trigger: Cron job (api/cron/reminders.ts calls runSendReminders() in packages/api/src/routers/index.ts)
Logic:

// In runSendReminders or a dedicated helper:
const unpaidWatchers = await db
  .select()
  .from(registration)
  .where(
    and(
      eq(registration.registrationType, "watcher"),
      eq(registration.paymentStatus, "pending"),
      isNull(registration.cancelledAt),
      isNull(registration.paymentReminderSentAt), // new column, see schema changes
      lte(registration.createdAt, new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)),
    )
  );

for (const reg of unpaidWatchers) {
  await sendPaymentReminderEmail({ to: reg.email, firstName: reg.firstName, managementToken: reg.managementToken });
  await db.update(registration).set({ paymentReminderSentAt: new Date() }).where(eq(registration.id, reg.id));
}

Email content (Dutch):

  • Subject: "Je inschrijving is bijna compleet — vergeet niet te betalen"
  • Body: friendly reminder that their drinkkaart payment is still open, link to /account.

3. Payment confirmation (on Mollie webhook success)

New function: sendPaymentConfirmationEmail
Trigger: apps/web/src/routes/api/webhook/mollie.ts, Branch B (registration payment), after paymentStatus is set to "paid".

Email content (Dutch):

  • Subject: "Betaling ontvangen — tot op 18 april!"
  • Body: confirm amount paid, drinkkaart activated, link to /account to view QR.

Schema changes

Add one nullable timestamp column to the registration table:

// packages/db/src/schema/registrations.ts
paymentReminderSentAt: integer("payment_reminder_sent_at", { mode: "timestamp" }),

Generate and apply a Drizzle migration after adding this field.


Acceptance criteria

  • Registration form no longer shows a login nudge or prefills from session.
  • After submitting the form (both roles), the user is always redirected to sign up / log in.
  • /account shows a "Betaal nu" CTA for watchers with paymentStatus === "pending".
  • Performers: no payment required; /account is the final step.
  • Existing Mollie webhook and claimRegistrationCredit logic are untouched.
  • Confirmation email updated (no checkout link, adds account creation link).
  • Payment reminder email sent 3 days after registration if watcher has not paid.
  • Payment confirmation email sent when Mollie webhook marks registration as paid.
  • New paymentReminderSentAt DB column + migration exist.
  • SuccessScreen component removed if unused; otherwise kept as-is.
## Context `EventRegistrationForm.tsx` and the surrounding registration components are currently too busy. The form tries to handle authentication nudges, optional account creation (dismissible modal), and a Mollie checkout redirect all in one shot, with varying behavior depending on whether the user is logged in. This creates confusion and edge cases. This issue specifies a simpler, linear flow and three new transactional emails. --- ## New User Flow ### Step 1 — Choose role User sees the existing `TypeSelector` (two cards): - **Artiest** — "Ik wil optreden" - **Bezoeker** — "Ik wil komen kijken" No sign-in / sign-up prompt at this stage. Remove the "Al een account?" nudge banner from `EventRegistrationForm.tsx`. ### Step 2 — Fill in info The regular `PerformerForm` / `WatcherForm` fields (name, email, phone, etc.) exactly as today. No changes to the field set itself. **Remove** the `isLoggedIn` / prefill logic from the forms — no more pre-filling from session and no read-only email field for logged-in users. (The user fills in their own info; the account they create in step 3 will be linked by email.) ### Step 3 — Bevestigen → mandatory account creation After the user taps **Bevestigen**: 1. `submitRegistration` is called as today (creates the DB row, sends confirmation email — see email section below). 2. Instead of showing the dismissible `AccountModal` or redirecting straight to Mollie, **always redirect the user to `/login?signup=1&email=<encoded-email>&next=/account`** (or a dedicated `/register` page if preferred). Account creation is **not optional**. 3. After sign-up the user lands on `/account`. > Performers do not need to pay, so for them `/account` is the final destination. > Watchers still owe payment (see step 4). ### Step 4 — Payment (Bezoeker only) On `/account`, if `registration.paymentStatus === "pending"` and `registration.registrationType === "watcher"`, show a **prominent "Betaal nu" CTA** that calls `getCheckoutUrl` and redirects to Mollie. This replaces the current direct post-form Mollie redirect. The user can close the browser and pay later — the outstanding payment is shown persistently on `/account` until resolved. > The existing `payment_status` column and Mollie webhook handler remain unchanged. --- ## What needs to change ### `apps/web/src/components/homepage/EventRegistrationForm.tsx` - Remove the "Al een account?" login nudge banner (lines 117–143). - Remove the `authClient.useSession()` call, the `isLoggedIn` / prefill logic (lines 48–71). - After `setSuccessState` / `onSuccess`, do **not** render `SuccessScreen`. Instead, redirect to `/login?signup=1&email=<email>&next=/account`. - The `successState` interface and `SuccessScreen` import can be removed from this file (SuccessScreen may still be used elsewhere, keep the component itself). ### `apps/web/src/components/registration/WatcherForm.tsx` - Remove the `AccountModal` interstitial that currently appears after successful registration. - Remove the direct Mollie `getCheckoutUrl` call + `window.location` redirect. - On success, call `onSuccess(token, email, name)` (same as `PerformerForm`) and let the parent handle the redirect. - Remove the `isLoggedIn` / prefill props (`prefillFirstName`, `prefillLastName`, `prefillEmail`, `isLoggedIn`); the form is always unauthenticated at this point. ### `apps/web/src/components/registration/PerformerForm.tsx` - Remove `prefillFirstName`, `prefillLastName`, `prefillEmail`, `isLoggedIn` props. - The submit flow is unchanged (calls `onSuccess`), but the parent now handles the account redirect. ### `apps/web/src/routes/account.tsx` - If `registration.paymentStatus === "pending"` and `registration.registrationType === "watcher"`, render a **"Betaal je inschrijving" card** (or banner) with: - Summary: drinkkaart amount + any gift. - A **"Betaal nu"** button that calls `orpc.getCheckoutUrl({ token: registration.managementToken })` and redirects to the returned `checkoutUrl`. - The existing `claimRegistrationCredit()` call on mount covers the case where the user already paid before creating an account — keep that. ### `apps/web/src/components/registration/SuccessScreen.tsx` - This component can be **deleted** unless it is used on the `/manage/<token>` page. Check usages first; if only used from `EventRegistrationForm`, remove it. --- ## New Email Triggers Three new transactional emails need to be added to `packages/api/src/email.ts` and called from the appropriate places. ### 1. Signup confirmation (on submit) **Trigger:** `submitRegistration` handler in `packages/api/src/routers/index.ts` — already calls `sendConfirmationEmail`. **No new email needed here** — this is the existing confirmation email. However, **update the template** to: - Not mention a payment link / checkout (that happens via the account page now). - Add a line: "Maak je account aan via [link] om je inschrijving te beheren en je drinkkaart te activeren." ### 2. Payment reminder after 3 days (Bezoeker only, unpaid) **New function:** `sendPaymentReminderEmail` **Trigger:** Cron job (`api/cron/reminders.ts` calls `runSendReminders()` in `packages/api/src/routers/index.ts`) **Logic:** ```ts // In runSendReminders or a dedicated helper: const unpaidWatchers = await db .select() .from(registration) .where( and( eq(registration.registrationType, "watcher"), eq(registration.paymentStatus, "pending"), isNull(registration.cancelledAt), isNull(registration.paymentReminderSentAt), // new column, see schema changes lte(registration.createdAt, new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)), ) ); for (const reg of unpaidWatchers) { await sendPaymentReminderEmail({ to: reg.email, firstName: reg.firstName, managementToken: reg.managementToken }); await db.update(registration).set({ paymentReminderSentAt: new Date() }).where(eq(registration.id, reg.id)); } ``` Email content (Dutch): - Subject: "Je inschrijving is bijna compleet — vergeet niet te betalen" - Body: friendly reminder that their drinkkaart payment is still open, link to `/account`. ### 3. Payment confirmation (on Mollie webhook success) **New function:** `sendPaymentConfirmationEmail` **Trigger:** `apps/web/src/routes/api/webhook/mollie.ts`, Branch B (registration payment), after `paymentStatus` is set to `"paid"`. Email content (Dutch): - Subject: "Betaling ontvangen — tot op 18 april!" - Body: confirm amount paid, drinkkaart activated, link to `/account` to view QR. --- ## Schema changes Add one nullable timestamp column to the `registration` table: ```ts // packages/db/src/schema/registrations.ts paymentReminderSentAt: integer("payment_reminder_sent_at", { mode: "timestamp" }), ``` Generate and apply a Drizzle migration after adding this field. --- ## Acceptance criteria - [x] Registration form no longer shows a login nudge or prefills from session. - [x] After submitting the form (both roles), the user is always redirected to sign up / log in. - [x] `/account` shows a "Betaal nu" CTA for watchers with `paymentStatus === "pending"`. - [ ] Performers: no payment required; `/account` is the final step. - [x] Existing Mollie webhook and `claimRegistrationCredit` logic are untouched. - [x] Confirmation email updated (no checkout link, adds account creation link). - [ ] Payment reminder email sent 3 days after registration if watcher has not paid. - [ ] Payment confirmation email sent when Mollie webhook marks registration as paid. - [x] New `paymentReminderSentAt` DB column + migration exist. - [x] `SuccessScreen` component removed if unused; otherwise kept as-is.
Author
Owner

All work is now complete and committed in 8221c7e.

What was done

Registration flow

  • EventRegistrationForm.tsx / WatcherForm.tsx / PerformerForm.tsx: removed session/login nudge/SuccessScreen; both roles now call redirectToSignup(email)/login?signup=1&email=<email>&next=/account after successful submit (account creation mandatory)
  • SuccessScreen.tsx deleted

Account page

  • account.tsx: added checkoutMutation + "Betaal nu" CTA card shown when registrationType === "watcher" AND paymentStatus === "pending" AND managementToken exists

DB

  • packages/db/src/schema/registrations.ts: paymentReminderSentAt column added
  • packages/db/src/migrations/0008_payment_reminder_sent_at.sql: migration created

Emails (packages/api/src/email.ts)

  • registrationConfirmationHtml / sendConfirmationEmail: now includes signupUrl (built from recipient email as /login?signup=1&email=…&next=/account); "Account aanmaken" CTA replaces checkout CTA
  • sendPaymentReminderEmail: new — sent to watchers with unpaid registration after 3 days
  • sendPaymentConfirmationEmail: new — sent after Mollie marks registration as paid

Cron reminders (packages/api/src/routers/index.ts)

  • runSendReminders() extended: after existing pre-registration reminders, now also queries registration for watchers where paymentStatus = "pending", paymentReminderSentAt IS NULL, createdAt ≤ now − 3 days; sends sendPaymentReminderEmail and stamps paymentReminderSentAt

Mollie webhook (apps/web/src/routes/api/webhook/mollie.ts)

  • After marking registration as paid, fires sendPaymentConfirmationEmail (fire-and-forget, won't block the 200 response to Mollie)
All work is now complete and committed in `8221c7e`. ## What was done ### Registration flow - `EventRegistrationForm.tsx` / `WatcherForm.tsx` / `PerformerForm.tsx`: removed session/login nudge/SuccessScreen; both roles now call `redirectToSignup(email)` → `/login?signup=1&email=<email>&next=/account` after successful submit (account creation mandatory) - `SuccessScreen.tsx` deleted ### Account page - `account.tsx`: added `checkoutMutation` + **"Betaal nu"** CTA card shown when `registrationType === "watcher"` AND `paymentStatus === "pending"` AND `managementToken` exists ### DB - `packages/db/src/schema/registrations.ts`: `paymentReminderSentAt` column added - `packages/db/src/migrations/0008_payment_reminder_sent_at.sql`: migration created ### Emails (`packages/api/src/email.ts`) - `registrationConfirmationHtml` / `sendConfirmationEmail`: now includes `signupUrl` (built from recipient email as `/login?signup=1&email=…&next=/account`); "Account aanmaken" CTA replaces checkout CTA - `sendPaymentReminderEmail`: new — sent to watchers with unpaid registration after 3 days - `sendPaymentConfirmationEmail`: new — sent after Mollie marks registration as paid ### Cron reminders (`packages/api/src/routers/index.ts`) - `runSendReminders()` extended: after existing pre-registration reminders, now also queries `registration` for watchers where `paymentStatus = "pending"`, `paymentReminderSentAt IS NULL`, `createdAt ≤ now − 3 days`; sends `sendPaymentReminderEmail` and stamps `paymentReminderSentAt` ### Mollie webhook (`apps/web/src/routes/api/webhook/mollie.ts`) - After marking registration as paid, fires `sendPaymentConfirmationEmail` (fire-and-forget, won't block the 200 response to Mollie)
zias closed this issue 2026-03-18 15:29:45 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: zias/kunstenkamp#6