1306 lines
36 KiB
Vue
1306 lines
36 KiB
Vue
<template>
|
||
<div>
|
||
<!-- HERO -->
|
||
<div class="hero">
|
||
<h1>Join Ghost Guild</h1>
|
||
<p>
|
||
Resources, events, and a community of people figuring it out. Everyone
|
||
gets everything. Pay what you can.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Already a member -->
|
||
<template v-if="isAuthenticated">
|
||
<div class="full-section">
|
||
<h2>You're already a member</h2>
|
||
<p class="section-intro">
|
||
Welcome back, {{ memberData?.name || "member" }}. You're part of Ghost
|
||
Guild in the
|
||
<span class="capitalize">{{
|
||
memberData?.circle || "community"
|
||
}}</span>
|
||
circle.
|
||
</p>
|
||
|
||
<div class="member-info-grid">
|
||
<DashedBox :hoverable="false">
|
||
<div class="section-label">Circle</div>
|
||
<div class="info-value capitalize">
|
||
{{ memberData?.circle || "Community" }}
|
||
</div>
|
||
</DashedBox>
|
||
<DashedBox :hoverable="false">
|
||
<div class="section-label">Contribution</div>
|
||
<div class="info-value">
|
||
${{ memberData?.contributionTier || "0" }} CAD/month
|
||
</div>
|
||
</DashedBox>
|
||
</div>
|
||
|
||
<div class="button-row">
|
||
<NuxtLink to="/member/dashboard" class="form-submit"
|
||
>Go to Dashboard</NuxtLink
|
||
>
|
||
<NuxtLink to="/member/profile" class="btn">Edit Profile</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<ParchmentInset>
|
||
<h2>Want to change your circle or contribution?</h2>
|
||
<p>
|
||
You can update your circle and adjust your monthly contribution at any
|
||
time from your profile settings.
|
||
</p>
|
||
<NuxtLink to="/member/profile" class="parchment-link"
|
||
>Update Membership Settings</NuxtLink
|
||
>
|
||
</ParchmentInset>
|
||
</template>
|
||
|
||
<!-- Not authenticated: show full join page -->
|
||
<template v-else>
|
||
<!-- CONTRIBUTION + SIGN UP (two columns) -->
|
||
<div v-if="currentStep === 1" class="join-two-col">
|
||
<!-- Left: Monthly Contribution -->
|
||
<div class="join-col">
|
||
<div class="section-label" style="margin-bottom: 12px">
|
||
{{ cadence === 'annual' ? 'Annual Contribution' : 'Monthly Contribution' }}
|
||
</div>
|
||
<h2>Pay what you can</h2>
|
||
<ul class="tier-list">
|
||
<li><span class="tier-amt">$0</span> I need support right now</li>
|
||
<li><span class="tier-amt">{{ formatTierAmount('5') }}</span> I can contribute</li>
|
||
<li>
|
||
<span class="tier-amt">{{ formatTierAmount('15') }}</span> I can sustain the community
|
||
(suggested)
|
||
</li>
|
||
<li><span class="tier-amt">{{ formatTierAmount('30') }}</span> I can support others too</li>
|
||
<li>
|
||
<span class="tier-amt">{{ formatTierAmount('50') }}</span> I want to sponsor multiple
|
||
members
|
||
</li>
|
||
</ul>
|
||
<p class="solidarity-note">
|
||
Pay what you can. If you can pay more, you're making room for
|
||
someone who can't.
|
||
</p>
|
||
<p class="circle-not-sure">
|
||
Not sure where you fit? Start with Community. You can always move
|
||
later.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Right: Become a member -->
|
||
<div class="join-col">
|
||
<h2>Become a member</h2>
|
||
<p class="form-intro">
|
||
You'll get a magic link to confirm your email. No passwords.
|
||
</p>
|
||
|
||
<!-- Error Message -->
|
||
<div v-if="errorMessage" class="error-box">
|
||
{{ errorMessage }}
|
||
</div>
|
||
|
||
<form @submit.prevent="handleSubmit">
|
||
<div class="form-stack">
|
||
<div class="form-group">
|
||
<label class="form-label" for="join-name">Full Name</label>
|
||
<input
|
||
id="join-name"
|
||
v-model="form.name"
|
||
class="form-input"
|
||
type="text"
|
||
placeholder="Your name"
|
||
required
|
||
>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="join-email">Email Address</label>
|
||
<input
|
||
id="join-email"
|
||
v-model="form.email"
|
||
class="form-input"
|
||
type="email"
|
||
placeholder="you@example.com"
|
||
required
|
||
>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Circle</label>
|
||
<div class="circle-radios">
|
||
<div class="circle-radio community">
|
||
<input
|
||
id="circle-community"
|
||
v-model="form.circle"
|
||
type="radio"
|
||
name="circle"
|
||
value="community"
|
||
>
|
||
<label for="circle-community">
|
||
<span
|
||
class="circle-label-name"
|
||
style="color: var(--c-community)"
|
||
>Community</span
|
||
>
|
||
<span class="circle-label-desc">Exploring</span>
|
||
</label>
|
||
</div>
|
||
<div class="circle-radio founder">
|
||
<input
|
||
id="circle-founder"
|
||
v-model="form.circle"
|
||
type="radio"
|
||
name="circle"
|
||
value="founder"
|
||
>
|
||
<label for="circle-founder">
|
||
<span
|
||
class="circle-label-name"
|
||
style="color: var(--c-founder)"
|
||
>Founder</span
|
||
>
|
||
<span class="circle-label-desc">Building</span>
|
||
</label>
|
||
</div>
|
||
<div class="circle-radio practitioner">
|
||
<input
|
||
id="circle-practitioner"
|
||
v-model="form.circle"
|
||
type="radio"
|
||
name="circle"
|
||
value="practitioner"
|
||
>
|
||
<label for="circle-practitioner">
|
||
<span
|
||
class="circle-label-name"
|
||
style="color: var(--c-practitioner)"
|
||
>Practitioner</span
|
||
>
|
||
<span class="circle-label-desc">Practicing</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Billing Cadence</label>
|
||
<div class="cadence-radios">
|
||
<div class="circle-radio">
|
||
<input
|
||
id="cadence-monthly"
|
||
v-model="cadence"
|
||
type="radio"
|
||
name="cadence"
|
||
value="monthly"
|
||
>
|
||
<label for="cadence-monthly">
|
||
<span class="circle-label-name">Monthly</span>
|
||
</label>
|
||
</div>
|
||
<div class="circle-radio">
|
||
<input
|
||
id="cadence-annual"
|
||
v-model="cadence"
|
||
type="radio"
|
||
name="cadence"
|
||
value="annual"
|
||
>
|
||
<label for="cadence-annual">
|
||
<span class="circle-label-name">Annual</span>
|
||
<span class="circle-label-desc">2 months free</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="join-contribution"
|
||
>{{ cadence === 'annual' ? 'Annual' : 'Monthly' }} Contribution</label
|
||
>
|
||
<USelectMenu
|
||
id="join-contribution"
|
||
v-model="form.contributionTier"
|
||
:items="contributionItems"
|
||
value-key="value"
|
||
:search-input="false"
|
||
class="zine-select"
|
||
:ui="{
|
||
content: 'tz-content',
|
||
item: 'tz-item',
|
||
}"
|
||
/>
|
||
</div>
|
||
<div class="form-group full-width">
|
||
<label class="checkbox-label">
|
||
<input
|
||
v-model="form.agreedToGuidelines"
|
||
type="checkbox"
|
||
>
|
||
<span>
|
||
I agree to the Ghost Guild
|
||
<NuxtLink to="/community-guidelines" target="_blank"
|
||
>Community Guidelines</NuxtLink
|
||
>.
|
||
</span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<button
|
||
class="form-submit"
|
||
type="submit"
|
||
:disabled="!isFormValid || isSubmitting"
|
||
>
|
||
<span v-if="isSubmitting">Processing...</span>
|
||
<span v-else-if="needsPayment">Continue to Payment</span>
|
||
<span v-else>Become a Member</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="form-note">
|
||
You can change your circle or contribution at any time from your
|
||
dashboard. Payment is handled securely through
|
||
<a href="https://www.helcim.com" target="_blank" rel="noopener"
|
||
>Helcim</a
|
||
>.
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HOW MEMBERSHIP WORKS -->
|
||
<ParchmentInset>
|
||
<h2>How membership works</h2>
|
||
<ul>
|
||
<li>Full access to the knowledge commons, Slack, and peer support</li>
|
||
<li>Free access to all Ghost Guild events</li>
|
||
<li>Equal access for every member, regardless of contribution</li>
|
||
<li>Your circle reflects where you are, not rank</li>
|
||
<li>Pay what you can ($0–$50+/month, separate from circle)</li>
|
||
<li>Higher contributions create solidarity spots for others</li>
|
||
</ul>
|
||
</ParchmentInset>
|
||
|
||
<!-- THREE CIRCLES -->
|
||
<div class="content-row">
|
||
<div class="content-block">
|
||
<div class="section-label" style="color: var(--c-community)">
|
||
Community
|
||
</div>
|
||
<h2>Exploring</h2>
|
||
<p>
|
||
For game workers curious about cooperatives and people exploring
|
||
alternative work models. You might be a solo developer, a student, a
|
||
researcher, or just someone who heard about this and wants to know
|
||
more. Start here.
|
||
</p>
|
||
</div>
|
||
<div class="content-block">
|
||
<div class="section-label" style="color: var(--c-founder)">
|
||
Founder
|
||
</div>
|
||
<h2>Building</h2>
|
||
<p>
|
||
For people actively building cooperative studios. You have a team,
|
||
or you are forming one. You are working through governance, legal
|
||
structure, revenue sharing, and all the hard parts. You want
|
||
structured support and peers doing the same thing.
|
||
</p>
|
||
</div>
|
||
<div class="content-block">
|
||
<div class="section-label" style="color: var(--c-practitioner)">
|
||
Practitioner
|
||
</div>
|
||
<h2>Practicing</h2>
|
||
<p>
|
||
For those already running cooperative studios or with deep
|
||
experience in cooperative practice. You are here to teach, advise,
|
||
mentor, and help shape the program itself. Alumni.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Payment -->
|
||
<div v-if="currentStep === 2" class="form-section">
|
||
<h2>Payment Information</h2>
|
||
<p class="form-intro">
|
||
You're signing up for the {{ selectedTier.label }} plan -- ${{
|
||
selectedTier.amount
|
||
}}
|
||
CAD / month
|
||
</p>
|
||
|
||
<!-- Error Message -->
|
||
<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="goBack">
|
||
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>
|
||
|
||
<!-- Step 3: Confirmation -->
|
||
<div v-if="currentStep === 3" class="form-section">
|
||
<h2>Welcome to Ghost Guild!</h2>
|
||
|
||
<div v-if="successMessage" class="success-box">
|
||
{{ successMessage }}
|
||
</div>
|
||
|
||
<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>{{ selectedTier.label }}</dd>
|
||
</div>
|
||
<div v-if="customerCode" class="details-row">
|
||
<dt>Member ID</dt>
|
||
<dd>{{ customerCode }}</dd>
|
||
</div>
|
||
</dl>
|
||
</DashedBox>
|
||
|
||
<p class="form-note" style="margin-top: 20px">
|
||
We've sent a confirmation email to {{ form.email }} with your
|
||
membership details.
|
||
</p>
|
||
|
||
<DashedBox :hoverable="false" style="margin-top: 16px">
|
||
<p class="redirect-note">
|
||
You will be automatically redirected to your dashboard in a few
|
||
seconds...
|
||
</p>
|
||
</DashedBox>
|
||
|
||
<div class="button-row" style="margin-top: 24px">
|
||
<NuxtLink to="/member/dashboard" class="form-submit"
|
||
>Go to Dashboard Now</NuxtLink
|
||
>
|
||
<button class="btn" @click="resetForm">
|
||
Register Another Member
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Flow overlay: covers the page from form submit through redirect -->
|
||
<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>{{ selectedTier.label }}</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>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
|
||
import { getCircleOptions } from "~/config/circles";
|
||
import {
|
||
requiresPayment,
|
||
getContributionTierByValue,
|
||
getTierAmount,
|
||
CONTRIBUTION_TIERS,
|
||
} from "~/config/contributions";
|
||
|
||
// Auth state
|
||
const { isAuthenticated, memberData, checkMemberStatus } = useAuth();
|
||
|
||
// Check authentication status on mount
|
||
onMounted(async () => {
|
||
await checkMemberStatus();
|
||
});
|
||
|
||
// Form state
|
||
const form = reactive({
|
||
email: "",
|
||
name: "",
|
||
circle: "community",
|
||
contributionTier: "15",
|
||
agreedToGuidelines: false,
|
||
billingAddress: {
|
||
street: "",
|
||
city: "",
|
||
province: "",
|
||
postalCode: "",
|
||
country: "CA",
|
||
},
|
||
});
|
||
|
||
// UI state
|
||
const isSubmitting = ref(false);
|
||
const currentStep = ref(1); // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation
|
||
const errorMessage = ref("");
|
||
const successMessage = ref("");
|
||
const cadence = ref("monthly"); // 'monthly' | 'annual'
|
||
|
||
// Flow overlay state — drives the post-submit full-viewport UI.
|
||
// 'idle' = overlay hidden; user is editing the form.
|
||
// 'creating-customer' | 'opening-payment' | 'processing-payment'
|
||
// | 'creating-subscription' = progress states, overlay shows a spinner + label.
|
||
// 'success' = overlay shows confirmation, auto-redirect is queued.
|
||
// 'error' = overlay shows error + Retry/Back buttons.
|
||
const flowState = ref("idle");
|
||
|
||
// Helcim state
|
||
const customerId = ref(null);
|
||
const customerCode = ref(null);
|
||
const subscriptionData = ref(null);
|
||
const paymentToken = ref(null);
|
||
|
||
// Circle options from central config
|
||
const circleOptions = getCircleOptions();
|
||
|
||
// Minimal labels for the dropdown — reactive to cadence.
|
||
// In annual mode, show both monthly and annual price so $50/yr (the $5 tier annual)
|
||
// is visually distinct from $500/yr (the $50 tier annual).
|
||
const contributionItems = computed(() => {
|
||
return Object.values(CONTRIBUTION_TIERS).map((tier) => {
|
||
const base = tier.amount;
|
||
if (base === 0) return { value: tier.value, label: "$0" };
|
||
const monthlyLabel = `$${base}/mo`;
|
||
const priceLabel =
|
||
cadence.value === "annual"
|
||
? `${monthlyLabel} → $${getTierAmount(tier, "annual")}/yr`
|
||
: monthlyLabel;
|
||
const hint = tier.value === "15" ? " (suggested)" : "";
|
||
return { value: tier.value, label: `${priceLabel}${hint}` };
|
||
});
|
||
});
|
||
|
||
const formatTierAmount = (value) => {
|
||
const tier = getContributionTierByValue(value);
|
||
if (!tier || tier.amount === 0) return "$0";
|
||
const amt = getTierAmount(tier, cadence.value);
|
||
const suffix = cadence.value === "annual" ? "/yr" : "/mo";
|
||
return `$${amt}${suffix}`;
|
||
};
|
||
|
||
// Initialize composables
|
||
const {
|
||
initializeHelcimPay,
|
||
verifyPayment,
|
||
cleanup: cleanupHelcimPay,
|
||
} = useHelcimPay();
|
||
|
||
// Form validation
|
||
const isFormValid = computed(() => {
|
||
return (
|
||
form.name &&
|
||
form.email &&
|
||
form.circle &&
|
||
form.contributionTier &&
|
||
form.agreedToGuidelines
|
||
);
|
||
});
|
||
|
||
// Check if payment is required
|
||
const needsPayment = computed(() => {
|
||
return requiresPayment(form.contributionTier);
|
||
});
|
||
|
||
// Get selected tier info
|
||
const selectedTier = computed(() => {
|
||
return getContributionTierByValue(form.contributionTier);
|
||
});
|
||
|
||
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 handleSubmit = async () => {
|
||
if (isSubmitting.value || !isFormValid.value) return;
|
||
|
||
isSubmitting.value = true;
|
||
errorMessage.value = "";
|
||
flowState.value = "creating-customer";
|
||
|
||
try {
|
||
// Create customer
|
||
const response = await $fetch("/api/helcim/customer", {
|
||
method: "POST",
|
||
body: {
|
||
name: form.name,
|
||
email: form.email,
|
||
circle: form.circle,
|
||
contributionTier: form.contributionTier,
|
||
agreedToGuidelines: form.agreedToGuidelines,
|
||
billingAddress: form.billingAddress,
|
||
},
|
||
});
|
||
|
||
if (!response.success) {
|
||
throw new Error("Failed to create account.");
|
||
}
|
||
|
||
customerId.value = response.customerId;
|
||
customerCode.value = response.customerCode;
|
||
|
||
// Free tier: no Helcim modal, go straight to subscription.
|
||
if (!needsPayment.value) {
|
||
flowState.value = "creating-subscription";
|
||
await createSubscription();
|
||
return;
|
||
}
|
||
|
||
// Paid tier: initialize HelcimPay session, then auto-open modal.
|
||
flowState.value = "opening-payment";
|
||
await initializeHelcimPay(customerId.value, customerCode.value, 0);
|
||
|
||
const paymentResult = await verifyPayment();
|
||
if (!paymentResult?.success) {
|
||
throw new Error("Payment was not completed.");
|
||
}
|
||
paymentToken.value = paymentResult.cardToken;
|
||
|
||
flowState.value = "processing-payment";
|
||
await $fetch("/api/helcim/verify-payment", {
|
||
method: "POST",
|
||
body: {
|
||
cardToken: paymentResult.cardToken,
|
||
customerId: customerId.value,
|
||
},
|
||
});
|
||
|
||
flowState.value = "creating-subscription";
|
||
const subscriptionResult = await createSubscription(
|
||
paymentResult.cardToken,
|
||
);
|
||
|
||
if (!subscriptionResult || subscriptionResult.success === false) {
|
||
// Payment succeeded but subscription couldn't be created.
|
||
// Keep overlay in success state; admin follow-up will reconcile.
|
||
successMessage.value =
|
||
"Payment successful. Subscription setup may need manual completion.";
|
||
flowState.value = "success";
|
||
}
|
||
} catch (error) {
|
||
console.error("Join flow error:", error);
|
||
errorMessage.value =
|
||
error.data?.message ||
|
||
error.message ||
|
||
"Something went wrong. Please try again.";
|
||
flowState.value = "error";
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
// Create subscription
|
||
const createSubscription = async (cardToken = null) => {
|
||
try {
|
||
const response = await $fetch("/api/helcim/subscription", {
|
||
method: "POST",
|
||
body: {
|
||
customerId: customerId.value,
|
||
customerCode: customerCode.value,
|
||
contributionTier: form.contributionTier,
|
||
cadence: cadence.value,
|
||
cardToken: cardToken,
|
||
},
|
||
});
|
||
|
||
if (response.success) {
|
||
subscriptionData.value = response.subscription;
|
||
currentStep.value = 3;
|
||
successMessage.value = "Your membership is active.";
|
||
|
||
// Check member status to ensure user is properly authenticated
|
||
await checkMemberStatus();
|
||
|
||
// Automatically redirect to welcome page after a short delay
|
||
setTimeout(() => {
|
||
navigateTo("/welcome");
|
||
}, 3000); // 3 second delay to show success message
|
||
} else {
|
||
throw new Error("Subscription creation failed - response not successful");
|
||
}
|
||
} catch (error) {
|
||
console.error("Subscription creation error:", error);
|
||
console.error("Error details:", {
|
||
message: error.message,
|
||
statusCode: error.statusCode,
|
||
statusMessage: error.statusMessage,
|
||
data: error.data,
|
||
});
|
||
console.error(
|
||
"Subscription creation completely failed, but payment was successful",
|
||
);
|
||
// Don't throw error - let the calling function handle progression
|
||
return {
|
||
success: false,
|
||
error:
|
||
error.data?.message || error.message || "Failed to create subscription",
|
||
};
|
||
}
|
||
};
|
||
|
||
const closeFlowOverlay = () => {
|
||
flowState.value = "idle";
|
||
errorMessage.value = "";
|
||
};
|
||
|
||
// Reset form
|
||
const resetForm = () => {
|
||
currentStep.value = 1;
|
||
customerId.value = null;
|
||
customerCode.value = null;
|
||
subscriptionData.value = null;
|
||
paymentToken.value = null;
|
||
errorMessage.value = "";
|
||
successMessage.value = "";
|
||
form.email = "";
|
||
form.name = "";
|
||
form.circle = "community";
|
||
form.contributionTier = "15";
|
||
};
|
||
|
||
// Cleanup on unmount
|
||
onUnmounted(() => {
|
||
cleanupHelcimPay();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ---- HERO ---- */
|
||
.hero {
|
||
padding: 48px 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.hero h1 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 36px;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
line-height: 1.15;
|
||
letter-spacing: -0.01em;
|
||
margin-bottom: 16px;
|
||
max-width: 540px;
|
||
}
|
||
.hero p {
|
||
color: var(--text-dim);
|
||
max-width: 460px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
/* ---- PARCHMENT LIST STYLES ---- */
|
||
:deep(.parchment-inset ul) {
|
||
list-style: none;
|
||
max-width: 560px;
|
||
padding: 0;
|
||
}
|
||
:deep(.parchment-inset ul li) {
|
||
font-size: 13px;
|
||
color: var(--parch-text-dim);
|
||
line-height: 1.75;
|
||
padding: 4px 0;
|
||
padding-left: 16px;
|
||
position: relative;
|
||
}
|
||
:deep(.parchment-inset ul li::before) {
|
||
content: "›";
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--candle-faint);
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.parchment-link {
|
||
color: var(--candle-faint);
|
||
font-size: 12px;
|
||
}
|
||
.parchment-link:hover {
|
||
color: var(--candle-dim);
|
||
}
|
||
|
||
/* ---- CONTENT ROW (three circles) ---- */
|
||
.content-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
align-items: stretch;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.content-block {
|
||
padding: 24px 28px;
|
||
border-right: 1px dashed var(--border);
|
||
min-width: 0;
|
||
overflow-wrap: break-word;
|
||
align-self: stretch;
|
||
}
|
||
.content-block:last-child {
|
||
border-right: none;
|
||
}
|
||
.content-block h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 8px;
|
||
}
|
||
.content-block p {
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
line-height: 1.65;
|
||
}
|
||
.circle-not-sure {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
margin-top: 10px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* ---- TWO-COLUMN JOIN LAYOUT ---- */
|
||
.join-two-col {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.join-col {
|
||
padding: 32px;
|
||
}
|
||
.join-col:first-child {
|
||
border-right: 1px dashed var(--border);
|
||
}
|
||
.join-col h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* ---- FULL-WIDTH SECTION ---- */
|
||
.full-section {
|
||
padding: 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.full-section h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 16px;
|
||
}
|
||
.section-intro {
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
line-height: 1.65;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* ---- TIER LIST (matches about page) ---- */
|
||
.tier-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
.tier-list li {
|
||
padding: 5px 0;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
border-bottom: 1px dashed var(--border);
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
.tier-list li:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.tier-amt {
|
||
color: var(--text-bright);
|
||
font-weight: 600;
|
||
min-width: 36px;
|
||
}
|
||
.solidarity-note {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.65;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* ---- FORM SECTION ---- */
|
||
.form-section {
|
||
padding: 32px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.form-section h2 {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-bottom: 4px;
|
||
}
|
||
.form-intro {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
margin-bottom: 24px;
|
||
line-height: 1.65;
|
||
}
|
||
.form-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
max-width: 600px;
|
||
}
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.form-label {
|
||
font-size: 10px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-faint);
|
||
}
|
||
.form-input {
|
||
background: var(--surface);
|
||
border: 1px dashed var(--border);
|
||
color: var(--text-bright);
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 13px;
|
||
padding: 10px 14px;
|
||
transition: border-color 0.2s;
|
||
outline: none;
|
||
width: 100%;
|
||
}
|
||
.form-input:focus {
|
||
border-color: var(--candle-dim);
|
||
border-style: solid;
|
||
}
|
||
.form-input::placeholder {
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
/* ---- CADENCE RADIOS ---- */
|
||
.cadence-radios {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
/* ---- CIRCLE RADIOS ---- */
|
||
.circle-radios {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
}
|
||
.circle-radio {
|
||
position: relative;
|
||
}
|
||
.circle-radio input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
.circle-radio label {
|
||
display: block;
|
||
border: 1px dashed var(--border);
|
||
padding: 14px 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
}
|
||
.circle-radio label:hover {
|
||
border-color: var(--candle-faint);
|
||
}
|
||
.circle-radio input:checked + label {
|
||
border-style: solid;
|
||
}
|
||
.circle-radio input:checked + label .circle-label-name {
|
||
color: var(--text-bright);
|
||
}
|
||
.circle-radio.community input:checked + label {
|
||
border-color: var(--c-community);
|
||
}
|
||
.circle-radio.founder input:checked + label {
|
||
border-color: var(--c-founder);
|
||
}
|
||
.circle-radio.practitioner input:checked + label {
|
||
border-color: var(--c-practitioner);
|
||
}
|
||
.circle-label-name {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
}
|
||
.circle-label-desc {
|
||
font-size: 10px;
|
||
color: var(--text-faint);
|
||
}
|
||
|
||
/* ---- CONTRIBUTION SELECT ---- */
|
||
.form-select {
|
||
background: var(--surface);
|
||
border: 1px dashed var(--border);
|
||
color: var(--text-bright);
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 13px;
|
||
padding: 10px 14px;
|
||
transition: border-color 0.2s;
|
||
outline: none;
|
||
width: 100%;
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238a7e6a' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 12px center;
|
||
cursor: pointer;
|
||
}
|
||
.form-select:focus {
|
||
border-color: var(--candle-dim);
|
||
border-style: solid;
|
||
}
|
||
.form-select option {
|
||
background: var(--surface);
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
/* ---- SUBMIT BUTTON ---- */
|
||
.form-submit {
|
||
display: inline-block;
|
||
background: var(--parch);
|
||
color: var(--parch-accent);
|
||
font-family: "Commit Mono", monospace;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
border: 1px solid var(--parch);
|
||
padding: 12px 28px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-decoration: none;
|
||
text-align: center;
|
||
}
|
||
.form-submit:hover {
|
||
background: var(--parch-hover);
|
||
border-color: var(--parch-hover);
|
||
color: var(--parch-text);
|
||
text-decoration: none;
|
||
}
|
||
.form-submit:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* ---- FORM NOTE ---- */
|
||
.form-note {
|
||
font-size: 11px;
|
||
color: var(--text-faint);
|
||
line-height: 1.6;
|
||
margin-top: 16px;
|
||
max-width: 460px;
|
||
}
|
||
.form-note a,
|
||
.form-note :deep(a) {
|
||
color: var(--candle-dim);
|
||
}
|
||
|
||
/* ---- CHECKBOX ---- */
|
||
.checkbox-label {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
line-height: 1.5;
|
||
}
|
||
.checkbox-label input {
|
||
margin-top: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
.checkbox-label a,
|
||
.checkbox-label :deep(a) {
|
||
color: var(--candle);
|
||
}
|
||
|
||
/* ---- ERROR & SUCCESS BOXES ---- */
|
||
.error-box {
|
||
border: 1px dashed var(--ember);
|
||
color: var(--ember);
|
||
padding: 12px 16px;
|
||
font-size: 12px;
|
||
margin-bottom: 20px;
|
||
max-width: 600px;
|
||
}
|
||
.success-box {
|
||
border: 1px dashed var(--green, var(--candle));
|
||
color: var(--green, var(--candle));
|
||
padding: 12px 16px;
|
||
font-size: 12px;
|
||
margin-bottom: 20px;
|
||
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;
|
||
color: var(--text-dim);
|
||
line-height: 1.65;
|
||
}
|
||
|
||
/* ---- REDIRECT NOTE ---- */
|
||
.redirect-note {
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
text-align: center;
|
||
}
|
||
|
||
/* ---- BUTTON ROW ---- */
|
||
.button-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* ---- MEMBER INFO GRID ---- */
|
||
.member-info-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
max-width: 500px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.info-value {
|
||
font-family: "Brygada 1918", serif;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-bright);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ---- UTILITY ---- */
|
||
.capitalize {
|
||
text-transform: capitalize;
|
||
}
|
||
|
||
/* ---- RESPONSIVE ---- */
|
||
@media (max-width: 768px) {
|
||
.content-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.content-block {
|
||
border-right: none;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.content-block:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.join-two-col {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.join-col:first-child {
|
||
border-right: none;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
.circle-radios {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.member-info-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.hero {
|
||
padding: 32px 20px;
|
||
}
|
||
.hero h1 {
|
||
font-size: 28px;
|
||
}
|
||
.full-section,
|
||
.form-section {
|
||
padding: 24px 20px;
|
||
}
|
||
.content-block {
|
||
padding: 20px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.button-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
|
||
.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>
|