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:
Jennie Robinson Faber 2026-04-20 12:34:59 +01:00
parent 493be2f3bc
commit a80728f0a8
10 changed files with 553 additions and 321 deletions

View file

@ -0,0 +1,191 @@
<template>
<Teleport to="body">
<div v-if="state !== 'idle'" class="signup-flow-overlay">
<div class="signup-flow-card">
<div class="signup-flow-step">{{ stepLabel }}</div>
<template v-if="isProgress">
<h2 class="signup-flow-heading">{{ progressHeading }}</h2>
<p class="signup-flow-body">
Please don't close this window. This usually takes a few seconds.
</p>
</template>
<template v-if="state === 'success'">
<h2 class="signup-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>{{ summary?.name }}</dd>
</div>
<div class="details-row">
<dt>Email</dt><dd>{{ summary?.email }}</dd>
</div>
<div class="details-row">
<dt>Circle</dt><dd class="capitalize">{{ summary?.circle }}</dd>
</div>
<div class="details-row">
<dt>Contribution</dt><dd>{{ summary?.contribution }}</dd>
</div>
</dl>
</DashedBox>
<p class="signup-flow-body" style="margin-top: 16px">
We've sent a confirmation email to {{ summary?.email }}. Redirecting
you to your dashboard...
</p>
<div class="button-row" style="margin-top: 20px">
<NuxtLink :to="dashboardHref" class="btn btn-primary">
Go to Dashboard Now
</NuxtLink>
</div>
</template>
<template v-if="state === 'error'">
<h2 class="signup-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="$emit('close')">
Back to form
</button>
</div>
</template>
</div>
</div>
</Teleport>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
state: { type: String, required: true },
summary: { type: Object, default: null },
errorMessage: { type: String, default: "" },
dashboardHref: { type: String, default: "/welcome" },
});
defineEmits(["close"]);
const PROGRESS_STATES = [
"creating-customer",
"opening-payment",
"processing-payment",
"creating-subscription",
];
const isProgress = computed(() => PROGRESS_STATES.includes(props.state));
const progressHeading = computed(() => {
switch (props.state) {
case "creating-customer": return "Creating your account...";
case "opening-payment": return "Opening secure payment...";
case "processing-payment": return "Confirming your card...";
case "creating-subscription": return "Activating your membership...";
default: return "";
}
});
const stepLabel = computed(() => {
switch (props.state) {
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 "";
}
});
</script>
<style scoped>
.signup-flow-overlay {
position: fixed;
inset: 0;
z-index: 50;
background: rgba(42, 32, 21, 0.72);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.signup-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;
}
.signup-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;
}
.signup-flow-heading {
font-family: var(--font-display);
font-size: 1.5rem;
color: var(--text-bright);
margin: 0 0 16px;
}
.signup-flow-body {
font-family: var(--font-body);
color: var(--text);
line-height: 1.5;
margin: 0;
}
.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;
}
.error-box {
border: 1px dashed var(--ember);
color: var(--ember);
padding: 12px 16px;
font-size: 12px;
}
.button-row {
display: flex;
gap: 12px;
align-items: center;
}
</style>

View file

@ -124,7 +124,39 @@
</div>
<div class="form-group full-width">
<label class="form-label" for="accept-contribution">Monthly Contribution</label>
<label class="form-label">Billing Cadence</label>
<div class="cadence-radios">
<div class="circle-radio">
<input
id="accept-cadence-annual"
v-model="cadence"
type="radio"
name="cadence"
value="annual"
>
<label for="accept-cadence-annual">
<span class="circle-label-name">Per Year</span>
</label>
</div>
<div class="circle-radio">
<input
id="accept-cadence-monthly"
v-model="cadence"
type="radio"
name="cadence"
value="monthly"
>
<label for="accept-cadence-monthly">
<span class="circle-label-name">Per Month</span>
</label>
</div>
</div>
</div>
<div class="form-group full-width">
<label class="form-label" for="accept-contribution">
Monthly Contribution
</label>
<div class="contribution-input-row">
<span class="contribution-currency">$</span>
<input
@ -152,6 +184,17 @@
<p class="field-note">Pay what you can. If you can pay more, you're making room for someone who can't.</p>
</div>
<div v-if="form.contributionAmount > 0" class="form-group full-width">
<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 &times; 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
@ -180,34 +223,14 @@
</form>
</div>
<!-- Payment Step -->
<div v-else-if="step === 'payment'" class="form-container">
<h1>Payment Information</h1>
<p class="form-intro">
You're signing up for ${{ form.contributionAmount }} CAD / month.
</p>
<div v-if="errorMessage" class="error-box">{{ errorMessage }}</div>
<DashedBox :hoverable="false">
<p class="payment-instruction">Click "Complete Payment" below to open the secure payment modal and verify your payment method.</p>
</DashedBox>
<div class="button-row" style="margin-top: 24px;">
<button class="btn" :disabled="isSubmitting" @click="step = 'form'">Back</button>
<button class="form-submit" :disabled="isSubmitting" @click="processPayment">
<span v-if="isSubmitting">Processing...</span>
<span v-else>Complete Payment</span>
</button>
</div>
</div>
<!-- Confirmation -->
<div v-else-if="step === 'confirmation'" class="center-box">
<h1>Welcome to Ghost Guild!</h1>
<p>Your membership is active. Redirecting to your dashboard...</p>
<NuxtLink to="/welcome" class="btn btn-primary" style="margin-top: 16px">Go to Dashboard</NuxtLink>
</div>
<!-- Flow overlay: covers the page through payment + redirect. -->
<SignupFlowOverlay
:state="flowState"
:summary="flowSummary"
:error-message="errorMessage"
dashboard-href="/member/dashboard?welcome=1"
@close="closeFlowOverlay"
/>
</div>
</template>
@ -221,6 +244,7 @@ import {
definePageMeta({ layout: false });
const { checkMemberStatus } = useAuth();
const { initializeHelcimPay, verifyPayment } = useHelcimPay();
const step = ref("verifying");
const errorMessage = ref("");
@ -228,6 +252,10 @@ const isSubmitting = ref(false);
const preRegId = ref(null);
const preRegEmail = ref("");
const token = ref("");
const cadence = ref("annual"); // 'monthly' | 'annual'
// Flow overlay state drives the post-submit full-viewport UI.
const flowState = ref("idle");
const form = reactive({
name: "",
@ -255,10 +283,29 @@ const needsPayment = computed(() => {
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
// Helcim state for paid tiers
const memberId = ref(null);
const customerId = ref(null);
const customerCode = ref(null);
const firstCharge = computed(() => {
const amount = form.contributionAmount || 0;
return cadence.value === "annual" ? amount * 12 : amount;
});
const formatContributionAmount = (amount) => {
if (!amount || amount === 0) return "$0";
const display = cadence.value === "annual" ? amount * 12 : amount;
const suffix = cadence.value === "annual" ? "/yr" : "/mo";
return `$${display}${suffix}`;
};
const flowSummary = computed(() => ({
name: form.name,
email: preRegEmail.value,
circle: form.circle,
contribution: formatContributionAmount(form.contributionAmount),
}));
const closeFlowOverlay = () => {
flowState.value = "idle";
errorMessage.value = "";
};
// On mount: extract token from fragment, verify
onMounted(async () => {
@ -294,9 +341,10 @@ const handleAccept = async () => {
isSubmitting.value = true;
errorMessage.value = "";
flowState.value = "creating-customer";
try {
const result = await $fetch("/api/invite/accept", {
const accepted = await $fetch("/api/invite/accept", {
method: "POST",
body: {
preRegistrationId: preRegId.value,
@ -311,90 +359,53 @@ const handleAccept = async () => {
},
});
memberId.value = result.member.id;
if (result.requiresPayment) {
// Need to create Helcim customer + payment
await setupPayment(result.member);
} else {
if (!accepted.requiresPayment) {
// Free tier session cookie already set by accept endpoint
await checkMemberStatus();
step.value = "confirmation";
setTimeout(() => navigateTo("/welcome"), 3000);
flowState.value = "success";
setTimeout(() => navigateTo("/member/dashboard?welcome=1"), 1500);
return;
}
} catch (err) {
errorMessage.value =
err.data?.statusMessage || "Failed to accept invitation. Please try again.";
} finally {
isSubmitting.value = false;
}
};
const setupPayment = async (member) => {
try {
// Create Helcim customer for paid tier
const customerResult = await $fetch("/api/helcim/customer", {
// Paid tier: initialize HelcimPay session, auto-open modal
flowState.value = "opening-payment";
await initializeHelcimPay(accepted.customerId, accepted.customerCode, 0);
const paymentResult = await verifyPayment();
if (!paymentResult?.success) {
throw new Error("Payment was not completed.");
}
flowState.value = "processing-payment";
await $fetch("/api/helcim/verify-payment", {
method: "POST",
body: {
name: member.name,
email: member.email,
circle: member.circle,
contributionAmount: form.contributionAmount,
cardToken: paymentResult.cardToken,
customerId: accepted.customerId,
},
});
customerId.value = customerResult.customerId;
customerCode.value = customerResult.customerCode;
flowState.value = "creating-subscription";
await $fetch("/api/helcim/subscription", {
method: "POST",
body: {
customerId: accepted.customerId,
customerCode: accepted.customerCode,
contributionAmount: form.contributionAmount,
cadence: cadence.value,
cardToken: paymentResult.cardToken,
},
});
// Initialize HelcimPay.js
const { initializeHelcimPay } = useHelcimPay();
await initializeHelcimPay(customerId.value, customerCode.value, 0);
step.value = "payment";
await checkMemberStatus();
flowState.value = "success";
setTimeout(() => navigateTo("/member/dashboard?welcome=1"), 1500);
} catch (err) {
errorMessage.value =
err.data?.statusMessage || "Failed to set up payment. Please try again.";
}
};
const processPayment = async () => {
if (isSubmitting.value) return;
isSubmitting.value = true;
errorMessage.value = "";
try {
const { verifyPayment } = useHelcimPay();
const paymentResult = await verifyPayment();
if (paymentResult.success) {
// Verify payment on server
await $fetch("/api/helcim/verify-payment", {
method: "POST",
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
});
// Create subscription
await $fetch("/api/helcim/subscription", {
method: "POST",
body: {
customerId: customerId.value,
customerCode: customerCode.value,
contributionAmount: form.contributionAmount,
cardToken: paymentResult.cardToken,
},
});
await checkMemberStatus();
step.value = "confirmation";
setTimeout(() => navigateTo("/welcome"), 3000);
}
} catch (err) {
errorMessage.value =
err.message || "Payment verification failed. Please try again.";
err.data?.statusMessage ||
err.message ||
"Failed to accept invitation. Please try again.";
flowState.value = "error";
} finally {
isSubmitting.value = false;
}
@ -558,6 +569,26 @@ textarea.form-input {
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;
@ -565,6 +596,12 @@ textarea.form-input {
gap: 8px;
}
.cadence-radios {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.circle-radio {
position: relative;
}
@ -682,5 +719,9 @@ textarea.form-input {
.circle-radios {
grid-template-columns: 1fr;
}
.cadence-radios {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -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 &times; 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>

View file

@ -72,9 +72,9 @@
</div>
</PageSection>
<!-- PAYMENT HISTORY (only when a Helcim customer exists) -->
<!-- PAYMENT HISTORY (only when there's actually a paid plan) -->
<PageSection
v-if="memberData.helcimCustomerId"
v-if="memberData.helcimCustomerId && (memberData.contributionAmount || 0) > 0"
divider="top"
>
<div class="section-label">Payment history</div>
@ -200,11 +200,11 @@
<div class="danger-zone">
<p>
Cancelling closes your account and ends access to member-only
spaces, including Slack. If you're cancelling because of a
spaces, including Slack.<template v-if="(memberData.contributionAmount || 0) > 0"> If you're cancelling because of a
money issue, the
<NuxtLink to="/community-guidelines">Solidarity Fund</NuxtLink>
and the $0 tier are always available reach out before you
go.
go.</template>
</p>
<div v-if="showCancelConfirm" class="cancel-confirm">
<p class="cancel-confirm-prompt">
@ -242,7 +242,7 @@
<div class="form-group">
<label class="form-label" for="account-contribution">
{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution
Monthly Contribution
</label>
<div class="contribution-input-row">
<span class="contribution-currency">$</span>
@ -269,8 +269,8 @@
</div>
<p v-if="guidanceLabel" class="contribution-guidance">{{ guidanceLabel }}</p>
</div>
<div class="tier-hint">
Changes take effect on your next billing cycle
<div v-if="contributionChangeHint" class="tier-hint">
{{ contributionChangeHint }}
</div>
<button
class="btn btn-primary btn-section"
@ -367,6 +367,20 @@ const cadence = computed(() => memberData.value?.billingCadence || 'monthly');
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
const contributionChangeHint = computed(() => {
const current = Number(memberData.value?.contributionAmount || 0);
const next = Number(form.contributionAmount || 0);
if (current === next) return "";
if (current === 0 && next > 0) {
const firstCharge = cadence.value === "annual" ? next * 12 : next;
return `You'll be charged $${firstCharge} today to start your subscription.`;
}
if (current > 0 && next === 0) {
return "Your paid subscription will be cancelled.";
}
return "Changes apply on your next billing cycle.";
});
const currentContributionLabel = computed(() => {
const amount = Number(memberData.value?.contributionAmount || 0);
if (!amount) return '$0';

View file

@ -33,7 +33,7 @@
<MemberStatusBanner />
<!-- Welcome Header -->
<PageHeader :title="`Welcome back, ${memberData?.name || ''}`">
<PageHeader :title="welcomeTitle">
<div class="dashboard-meta">
<CircleBadge :circle="memberData?.circle || 'community'" />
<span>${{ memberData?.contributionAmount ?? 0 }} CAD/mo</span>
@ -221,6 +221,15 @@
const { memberData, checkMemberStatus } = useAuth();
const { isActive, statusConfig, isPendingPayment, canPeerSupport } =
useMemberStatus();
const route = useRoute();
const isNewSignup = computed(() => route.query.welcome === "1");
const welcomeTitle = computed(() => {
const name = memberData.value?.name || "";
return isNewSignup.value
? `Welcome to Ghost Guild, ${name}`
: `Welcome back, ${name}`;
});
const { completePayment, isProcessingPayment } = useMemberPayment();
const { trackGoal, isComplete: onboardingComplete } = useOnboarding();

View file

@ -1,3 +1,3 @@
<script setup>
await navigateTo('/member/dashboard', { redirectCode: 301 })
await navigateTo('/member/dashboard?welcome=1', { redirectCode: 301 })
</script>