From 313b8598dfeb022971a2169d5f3806978516e58a Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Thu, 30 Apr 2026 14:39:47 +0100 Subject: [PATCH 1/3] fix(launch-flow): align Slack-wait copy across join, dashboard, welcome email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /join "How membership works" lists community (not Slack) as a benefit; adds a note that Slack invitations come in monthly onboarding waves. - Dashboard slack-coming note drops "2–3 weeks" timeline; uses the same monthly-waves phrasing. - Welcome email no longer points new members to Slack (which they don't yet have access to); directs them to reply instead. --- app/pages/join.vue | 6 +++++- app/pages/member/dashboard.vue | 4 ++-- server/utils/resend.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/pages/join.vue b/app/pages/join.vue index a76c316..67bc0d6 100644 --- a/app/pages/join.vue +++ b/app/pages/join.vue @@ -317,13 +317,17 @@

How membership works

+

+ Community connection happens in our Slack workspace, joined in monthly + onboarding waves — there may be a short wait after you join. +

diff --git a/app/pages/member/dashboard.vue b/app/pages/member/dashboard.vue index 26c0ad9..de91d71 100644 --- a/app/pages/member/dashboard.vue +++ b/app/pages/member/dashboard.vue @@ -39,8 +39,8 @@ ${{ memberData?.contributionAmount ?? 0 }} CAD/mo

- Slack workspace access is part of your membership. Your invitation - typically arrives within 2–3 weeks of joining. + Slack workspace access is part of your membership. Invitations are + sent in monthly onboarding waves — we'll be in touch.

diff --git a/server/utils/resend.js b/server/utils/resend.js index e3cada2..ce79e94 100644 --- a/server/utils/resend.js +++ b/server/utils/resend.js @@ -282,7 +282,7 @@ Welcome to Ghost Guild! You're now part of the ${member.circle} circle. Sign in to your dashboard to get started: ${baseUrl}/member/dashboard -If you have questions, reach out to jennie + eileen on Slack or reply to this email.`, +If you have questions, just reply to this email.`, }); if (error) { From d4000c18cf0850c30e9e1c34d5dcc0d498bb75ad Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Thu, 30 Apr 2026 14:40:13 +0100 Subject: [PATCH 2/3] fix(launch-flow): send welcome email on free /accept-invite activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free invite acceptance previously created a Member and signed them in without sending the welcome email — pre-registrants got nothing as the join confirmation. Wire sendWelcomeEmail into the free branch matching the pattern in members/create.post.js. Paid /accept-invite activations continue to receive the welcome email via /api/helcim/subscription on the pending_payment → active transition, so this only changes the free path. --- server/api/invite/accept.post.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/api/invite/accept.post.js b/server/api/invite/accept.post.js index 84d5db6..27e5109 100644 --- a/server/api/invite/accept.post.js +++ b/server/api/invite/accept.post.js @@ -5,6 +5,7 @@ import { connectDB } from '../../utils/mongoose.js' import { setAuthCookie } from '../../utils/auth.js' import { assignMemberNumber } from '../../utils/memberNumber.js' import { createHelcimCustomer } from '../../utils/helcim.js' +import { sendWelcomeEmail } from '../../utils/resend.js' export default defineEventHandler(async (event) => { const body = await validateBody(event, inviteAcceptSchema) @@ -88,6 +89,15 @@ export default defineEventHandler(async (event) => { // For free tier, redirect to welcome if (body.contributionAmount === 0) { await autoFlagPreExistingSlackAccess(member) + try { + await sendWelcomeEmail(member) + logActivity(member._id, 'email_sent', { + emailType: 'welcome', + subject: 'Welcome to Ghost Guild' + }) + } catch (emailError) { + console.error('Failed to send welcome email:', emailError) + } return { success: true, requiresPayment: false, From da5e7efcb710ccce0e6624d5e12f7f0ce09e058c Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Thu, 30 Apr 2026 14:43:02 +0100 Subject: [PATCH 3/3] fix(launch-flow): auto-link /join signups to existing PreRegistration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a /join submitter's email matches a pending/selected/invited PreRegistration, mark the pre-reg as accepted and link memberId to the new Member. Prevents the same person from appearing as both an active member and an unaccepted pre-registrant. Silent — no email, no UI. Adds the PreRegistration mock to helcim-customer and free-signup-flow test suites, since both invoke the customer handler at runtime. --- server/api/helcim/customer.post.js | 27 +++++++++++++++++++++++ tests/server/api/free-signup-flow.test.js | 3 +++ tests/server/api/helcim-customer.test.js | 3 +++ 3 files changed, 33 insertions(+) diff --git a/server/api/helcim/customer.post.js b/server/api/helcim/customer.post.js index d0fc95d..2d09ff3 100644 --- a/server/api/helcim/customer.post.js +++ b/server/api/helcim/customer.post.js @@ -2,6 +2,7 @@ import { getRequestHeader, getRequestIP } from 'h3' import Member from '../../models/member.js' import { connectDB } from '../../utils/mongoose.js' import { createHelcimCustomer } from '../../utils/helcim.js' +import PreRegistration from '../../models/preRegistration.js' import { sendMagicLink } from '../../utils/magicLink.js' import { setPaymentBridgeCookie } from '../../utils/auth.js' import { rateLimit } from '../../utils/rateLimit.js' @@ -82,6 +83,32 @@ export default defineEventHandler(async (event) => { }) } + // If this email matches a pending pre-registrant, mark the PreRegistration + // as accepted and link it to the new Member. Silent — keeps /join and + // /admin/pre-registrants from showing the same person twice. + try { + const preReg = await PreRegistration.findOne({ email: normalizedEmail }) + if ( + preReg && + !preReg.memberId && + ['pending', 'selected', 'invited'].includes(preReg.status) + ) { + await PreRegistration.findByIdAndUpdate( + preReg._id, + { + $set: { + status: 'accepted', + acceptedAt: new Date(), + memberId: member._id, + }, + }, + { runValidators: false } + ) + } + } catch (linkError) { + console.error('Failed to link PreRegistration to new member:', linkError) + } + await sendMagicLink(normalizedEmail, { subject: 'Verify your Ghost Guild signup', intro: 'Verify your email to finish your Ghost Guild signup:', diff --git a/tests/server/api/free-signup-flow.test.js b/tests/server/api/free-signup-flow.test.js index 521c0b2..bee73b3 100644 --- a/tests/server/api/free-signup-flow.test.js +++ b/tests/server/api/free-signup-flow.test.js @@ -20,6 +20,9 @@ vi.mock('../../../server/models/member.js', () => ({ findOneAndUpdate: vi.fn() } })) +vi.mock('../../../server/models/preRegistration.js', () => ({ + default: { findOne: vi.fn().mockResolvedValue(null), findByIdAndUpdate: vi.fn() } +})) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/helcim.js', () => ({ createHelcimCustomer: vi.fn(), diff --git a/tests/server/api/helcim-customer.test.js b/tests/server/api/helcim-customer.test.js index cba7df5..a023c27 100644 --- a/tests/server/api/helcim-customer.test.js +++ b/tests/server/api/helcim-customer.test.js @@ -12,6 +12,9 @@ import { createMockEvent } from '../helpers/createMockEvent.js' vi.mock('../../../server/models/member.js', () => ({ default: { findOne: vi.fn(), create: vi.fn(), findByIdAndUpdate: vi.fn() } })) +vi.mock('../../../server/models/preRegistration.js', () => ({ + default: { findOne: vi.fn().mockResolvedValue(null), findByIdAndUpdate: vi.fn() } +})) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/helcim.js', () => ({ createHelcimCustomer: vi.fn()