feat(signup): unify cadence UX across accept-invite, join, and account
Extract shared SignupFlowOverlay component. Static "Monthly Contribution" label on all three contribution inputs (was misleadingly dynamic). "Per Year"/"Per Month" toggle copy; Per Year default on accept-invite, Per Month default on join. Live billing-summary card on both signup flows. Welcome-heading on dashboard via ?welcome=1 for new signups. $0-member polish on account page (hide payment-history + Solidarity Fund prompts). State-aware contribution-change hint. Invite accept now creates Helcim customer and sets auth cookie server-side for both free and paid branches. Pre-registrant invite + /join signup flows manually verified against Cleo Nguyen preReg and $0-$50 variants.
This commit is contained in:
parent
493be2f3bc
commit
a80728f0a8
10 changed files with 553 additions and 321 deletions
|
|
@ -194,7 +194,7 @@
|
|||
value="monthly"
|
||||
>
|
||||
<label for="cadence-monthly">
|
||||
<span class="circle-label-name">Monthly</span>
|
||||
<span class="circle-label-name">Per Month</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="circle-radio">
|
||||
|
|
@ -206,14 +206,14 @@
|
|||
value="annual"
|
||||
>
|
||||
<label for="cadence-annual">
|
||||
<span class="circle-label-name">Annual</span>
|
||||
<span class="circle-label-name">Per Year</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="join-contribution">
|
||||
{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution
|
||||
Monthly Contribution
|
||||
</label>
|
||||
<div class="contribution-input-row">
|
||||
<span class="contribution-currency">$</span>
|
||||
|
|
@ -240,6 +240,16 @@
|
|||
</div>
|
||||
<p v-if="guidanceLabel" class="contribution-guidance">{{ guidanceLabel }}</p>
|
||||
</div>
|
||||
<div v-if="form.contributionAmount > 0" class="form-group">
|
||||
<div class="billing-summary">
|
||||
<p class="billing-summary-line">
|
||||
You'll be charged <strong>${{ firstCharge }} today</strong><span v-if="cadence === 'annual'"> (${{ form.contributionAmount }}/month × 12)</span>.
|
||||
</p>
|
||||
<p class="billing-summary-line">
|
||||
Then <strong>${{ firstCharge }} every {{ cadence === 'annual' ? 'year' : 'month' }}</strong>, until you cancel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
|
|
@ -334,84 +344,12 @@
|
|||
<!-- Flow overlay: covers the page from form submit through redirect.
|
||||
Lives outside v-if/v-else so it survives the auth state flip that
|
||||
fires after checkMemberStatus() at the end of createSubscription. -->
|
||||
<Teleport to="body">
|
||||
<div v-if="flowState !== 'idle'" class="join-flow-overlay">
|
||||
<div class="join-flow-card">
|
||||
<div class="join-flow-step">{{ flowStepLabel }}</div>
|
||||
|
||||
<!-- Progress states -->
|
||||
<template
|
||||
v-if="[
|
||||
'creating-customer',
|
||||
'opening-payment',
|
||||
'processing-payment',
|
||||
'creating-subscription',
|
||||
].includes(flowState)"
|
||||
>
|
||||
<h2 class="join-flow-heading">
|
||||
{{
|
||||
flowState === "creating-customer"
|
||||
? "Creating your account..."
|
||||
: flowState === "opening-payment"
|
||||
? "Opening secure payment..."
|
||||
: flowState === "processing-payment"
|
||||
? "Confirming your card..."
|
||||
: "Activating your membership..."
|
||||
}}
|
||||
</h2>
|
||||
<p class="join-flow-body">
|
||||
Please don't close this window. This usually takes a few seconds.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- Success state -->
|
||||
<template v-if="flowState === 'success'">
|
||||
<h2 class="join-flow-heading">Welcome to Ghost Guild!</h2>
|
||||
<DashedBox :hoverable="false">
|
||||
<div class="section-label" style="margin-bottom: 12px">
|
||||
Membership Details
|
||||
</div>
|
||||
<dl class="details-list">
|
||||
<div class="details-row">
|
||||
<dt>Name</dt><dd>{{ form.name }}</dd>
|
||||
</div>
|
||||
<div class="details-row">
|
||||
<dt>Email</dt><dd>{{ form.email }}</dd>
|
||||
</div>
|
||||
<div class="details-row">
|
||||
<dt>Circle</dt><dd class="capitalize">{{ form.circle }}</dd>
|
||||
</div>
|
||||
<div class="details-row">
|
||||
<dt>Contribution</dt><dd>{{ formatContributionAmount(form.contributionAmount) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</DashedBox>
|
||||
<p class="join-flow-body" style="margin-top: 16px">
|
||||
We've sent a confirmation email to {{ form.email }}. Redirecting
|
||||
you to your dashboard...
|
||||
</p>
|
||||
<div class="button-row" style="margin-top: 20px">
|
||||
<NuxtLink to="/welcome" class="form-submit">
|
||||
Go to Dashboard Now
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error state -->
|
||||
<template v-if="flowState === 'error'">
|
||||
<h2 class="join-flow-heading">We couldn't complete your signup</h2>
|
||||
<div v-if="errorMessage" class="error-box">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div class="button-row" style="margin-top: 20px">
|
||||
<button class="btn" @click="closeFlowOverlay">
|
||||
Back to form
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<SignupFlowOverlay
|
||||
:state="flowState"
|
||||
:summary="flowSummary"
|
||||
:error-message="errorMessage"
|
||||
@close="closeFlowOverlay"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -503,23 +441,18 @@ const needsPayment = computed(() => {
|
|||
|
||||
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
|
||||
|
||||
const flowStepLabel = computed(() => {
|
||||
switch (flowState.value) {
|
||||
case "creating-customer":
|
||||
case "opening-payment":
|
||||
return "Step 2 of 3 — Payment";
|
||||
case "processing-payment":
|
||||
case "creating-subscription":
|
||||
return "Step 2 of 3 — Finalizing";
|
||||
case "success":
|
||||
return "Step 3 of 3 — Welcome";
|
||||
case "error":
|
||||
return "Something went wrong";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
const firstCharge = computed(() => {
|
||||
const amount = form.contributionAmount || 0;
|
||||
return cadence.value === "annual" ? amount * 12 : amount;
|
||||
});
|
||||
|
||||
const flowSummary = computed(() => ({
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
circle: form.circle,
|
||||
contribution: formatContributionAmount(form.contributionAmount),
|
||||
}));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting.value || !isFormValid.value) return;
|
||||
|
||||
|
|
@ -918,6 +851,26 @@ onUnmounted(() => {
|
|||
color: var(--ink-soft, currentColor);
|
||||
}
|
||||
|
||||
/* ---- BILLING SUMMARY ---- */
|
||||
.billing-summary {
|
||||
padding: 12px 16px;
|
||||
border: 1px dashed var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
.billing-summary-line {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
.billing-summary-line + .billing-summary-line {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.billing-summary-line strong {
|
||||
color: var(--text-bright);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- CIRCLE RADIOS ---- */
|
||||
.circle-radios {
|
||||
display: grid;
|
||||
|
|
@ -1073,26 +1026,6 @@ onUnmounted(() => {
|
|||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* ---- DETAILS LIST (confirmation) ---- */
|
||||
.details-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.details-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: 13px;
|
||||
}
|
||||
.details-row dt {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.details-row dd {
|
||||
color: var(--text-bright);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- PAYMENT INSTRUCTION ---- */
|
||||
.payment-instruction {
|
||||
font-size: 13px;
|
||||
|
|
@ -1183,48 +1116,4 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.join-flow-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
background: rgba(42, 32, 21, 0.72); /* --parch @ 72% */
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.join-flow-card {
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--border);
|
||||
padding: 32px;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.join-flow-step {
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.join-flow-heading {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-bright);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.join-flow-body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue