ghostguild-org/app/pages/join.vue

717 lines
23 KiB
Vue

<template>
<div>
<!-- Page Header -->
<PageHeader
title="Join Ghost Guild"
subtitle="Become a member of our community and start building a more worker-centric future for games."
theme="gray"
size="large"
/>
<!-- Membership Sign Up Form -->
<section class="py-20 bg-[--ui-bg]">
<UContainer class="max-w-4xl">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
Membership Sign Up
</h2>
<p class="text-lg text-[--ui-text]">
Choose your circle to connect with others at your stage. Choose your
contribution based on what you can afford. Everyone gets full
access.
</p>
</div>
<!-- Step Indicators -->
<div class="flex justify-center mb-8">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 1
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
1
</div>
<span
class="ml-2 font-medium"
:class="
currentStep === 1 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Information
</span>
</div>
<div v-if="needsPayment" class="w-16 h-1 bg-neutral-200">
<div
class="h-full bg-neutral-900 transition-all"
:style="{ width: currentStep >= 2 ? '100%' : '0%' }"
/>
</div>
<div v-if="needsPayment" class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 2
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
2
</div>
<span
class="ml-2 font-medium"
:class="
currentStep === 2 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Payment
</span>
</div>
<div class="w-16 h-1 bg-neutral-200">
<div
class="h-full bg-neutral-900 transition-all"
:style="{ width: currentStep >= 3 ? '100%' : '0%' }"
/>
</div>
<div class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 3
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
<span v-if="needsPayment">3</span>
<span v-else>2</span>
</div>
<span
class="ml-2 font-medium"
:class="
currentStep === 3 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Confirmation
</span>
</div>
</div>
</div>
<!-- Error Message -->
<div v-if="errorMessage" class="mb-6">
<UAlert color="red" :title="errorMessage" />
</div>
<!-- Step 1: Information -->
<div v-if="currentStep === 1" class="bg-[--ui-bg-elevated]">
<UForm :state="form" class="space-y-8" @submit="handleSubmit">
<!-- Personal Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField label="Full Name" name="name" required>
<UInput
v-model="form.name"
placeholder="Enter your full name"
size="xl"
class="w-full"
/>
</UFormField>
<UFormField label="Email Address" name="email" required>
<UInput
v-model="form.email"
type="email"
size="xl"
class="w-full"
placeholder="Enter your email address"
/>
</UFormField>
</div>
<!-- Circle Selection -->
<div>
<h3 class="text-lg font-semibold mb-4">Choose Your Circle</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label
v-for="option in circleOptions"
:key="option.value"
class="flex flex-col p-6 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md"
:class="
form.circle === option.value
? 'border-neutral-900 bg-[--ui-bg]'
: 'border-neutral-200 hover:border-neutral-400'
"
>
<input
v-model="form.circle"
type="radio"
:value="option.value"
name="circle"
class="mb-3"
/>
<div class="font-medium text-lg mb-2">{{ option.label }}</div>
<div class="text-sm text-[--ui-text-muted]">
{{ option.description }}
</div>
</label>
</div>
<p class="text-sm text-[--ui-text-muted] mt-3 italic">
Not sure? Start with Community - you can always move.
</p>
</div>
<!-- Contribution Selection -->
<div>
<UFormField
label="Choose Your Monthly Contribution"
name="contributionTier"
required
>
<USelect
v-model="form.contributionTier"
:items="contributionOptions"
value-key="value"
size="xl"
class="w-full"
/>
</UFormField>
</div>
<!-- Submit Button -->
<div class="flex justify-center pt-6">
<UButton
type="submit"
:loading="isSubmitting"
:disabled="!isFormValid"
size="xl"
class="px-12"
>
{{
needsPayment ? "Continue to Payment" : "Complete Registration"
}}
</UButton>
</div>
</UForm>
</div>
<!-- Step 2: Payment -->
<div
v-if="currentStep === 2"
class="bg-[--ui-bg-elevated] rounded-xl p-8"
>
<div class="mb-6">
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
Payment Information
</h3>
<p class="text-[--ui-text-muted]">
You're signing up for the {{ selectedTier.label }} plan
</p>
<p class="text-lg font-semibold text-[--ui-text] mt-2">
${{ selectedTier.amount }} CAD / month
</p>
</div>
<!-- Payment Instructions -->
<div class="bg-[--ui-bg] rounded-lg p-6 mb-6">
<p class="text-[--ui-text]">
Click "Complete Payment" below to open the secure payment modal
and verify your payment method.
</p>
</div>
<!-- Action Buttons -->
<div class="flex justify-between pt-6">
<UButton
variant="outline"
size="lg"
@click="goBack"
:disabled="isSubmitting"
>
Back
</UButton>
<UButton size="lg" :loading="isSubmitting" @click="processPayment">
Complete Payment
</UButton>
</div>
</div>
<!-- Step 3: Confirmation -->
<div
v-if="currentStep === 3"
class="bg-[--ui-bg-elevated] rounded-xl p-8"
>
<div class="text-center">
<div
class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6"
>
<svg
class="w-10 h-10 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 class="text-2xl font-bold text-[--ui-text] mb-4">
Welcome to Ghost Guild!
</h3>
<div v-if="successMessage" class="mb-6">
<UAlert color="green" :title="successMessage" />
</div>
<div class="bg-[--ui-bg] rounded-lg p-6 mb-6 text-left">
<h4 class="font-semibold mb-3">Membership Details:</h4>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-[--ui-text-muted]">Name:</dt>
<dd class="font-medium">{{ form.name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[--ui-text-muted]">Email:</dt>
<dd class="font-medium">{{ form.email }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[--ui-text-muted]">Circle:</dt>
<dd class="font-medium capitalize">{{ form.circle }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-[--ui-text-muted]">Contribution:</dt>
<dd class="font-medium">{{ selectedTier.label }}</dd>
</div>
<div v-if="customerCode" class="flex justify-between">
<dt class="text-[--ui-text-muted]">Member ID:</dt>
<dd class="font-medium">{{ customerCode }}</dd>
</div>
</dl>
</div>
<p class="text-[--ui-text-muted] mb-4">
We've sent a confirmation email to {{ form.email }} with your
membership details.
</p>
<div class="bg-[--ui-bg] rounded-lg p-4 mb-8">
<p class="text-[--ui-text] text-center">
You will be automatically redirected to your dashboard in a few
seconds...
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton to="/member/dashboard" size="lg" class="px-8">
Go to Dashboard Now
</UButton>
<UButton variant="outline" size="lg" @click="resetForm">
Register Another Member
</UButton>
</div>
</div>
</div>
</UContainer>
</section>
<!-- How Ghost Guild Works -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
How Ghost Guild Works
</h2>
<p class="text-lg text-[--ui-text]">
Every member gets everything. Your circle helps you find relevant
content and peers. Your contribution helps sustain our solidarity
economy.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Full Access -->
<div class="bg-[--ui-bg] rounded-xl p-6">
<h3 class="text-xl font-semibold mb-4 text-[--ui-text]">
Full Access
</h3>
<ul class="text-[--ui-text] space-y-2">
<li>Complete resource library</li>
<li>All workshops and events</li>
<li>Slack community</li>
<li>Voting rights</li>
<li>Peer support opportunities</li>
</ul>
</div>
<!-- Circle-Specific Guidance -->
<div class="bg-[--ui-bg] rounded-xl p-6">
<h3 class="text-xl font-semibold mb-4 text-[--ui-text]">
Circle-Specific Guidance
</h3>
<ul class="text-[--ui-text] space-y-2">
<li>Curated resources for your stage</li>
<li>Connection with peers on similar journeys</li>
<li>Relevant workshop recommendations</li>
<li>Targeted support for your challenges</li>
</ul>
</div>
</div>
</div>
</UContainer>
</section>
<!-- How to Join -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="space-y-8">
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
1
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Pick your circle
</h3>
<p class="text-[--ui-text]">
Where are you in your co-op journey? Select based on where you
are in your cooperative journey - exploring, building, or
practicing. Not sure? Start with Community.
</p>
</div>
</div>
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
2
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Choose your contribution
</h3>
<p class="text-[--ui-text]">
What can you afford? ($0-50+/month) Choose based on your
financial capacity. From $0 for those who need support to $50+
for those who can sponsor others. You can adjust anytime.
</p>
</div>
</div>
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
3
</div>
</div>
<div class="flex-1">
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Join the community
</h3>
<p class="text-[--ui-text]">
Get instant access to everything. Fill out your profile, agree
to our community guidelines, and complete payment (if
applicable). You'll get instant access to our community.
</p>
</div>
</div>
</div>
</div>
</UContainer>
</section>
</div>
</template>
<script setup>
import { reactive, ref, computed, onMounted, onUnmounted } from "vue";
import { getCircleOptions } from "~/config/circles";
import {
getContributionOptions,
requiresPayment,
getContributionTierByValue,
} from "~/config/contributions";
// Form state
const form = reactive({
email: "",
name: "",
circle: "community",
contributionTier: "15",
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("");
// 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();
// Contribution options from central config
const contributionOptions = getContributionOptions();
// Initialize composables
const {
initializeHelcimPay,
verifyPayment,
cleanup: cleanupHelcimPay,
} = useHelcimPay();
const { checkMemberStatus } = useAuth();
// Form validation
const isFormValid = computed(() => {
return form.name && form.email && form.circle && form.contributionTier;
});
// Check if payment is required
const needsPayment = computed(() => {
return requiresPayment(form.contributionTier);
});
// Get selected tier info
const selectedTier = computed(() => {
return getContributionTierByValue(form.contributionTier);
});
// Step 1: Create customer
const handleSubmit = async () => {
if (isSubmitting.value || !isFormValid.value) return;
isSubmitting.value = true;
errorMessage.value = "";
try {
// Create customer in Helcim
const response = await $fetch("/api/helcim/customer", {
method: "POST",
body: {
name: form.name,
email: form.email,
circle: form.circle,
contributionTier: form.contributionTier,
billingAddress: form.billingAddress,
},
});
if (response.success) {
console.log("Customer response:", response);
customerId.value = response.customerId;
customerCode.value = response.customerCode;
// Token is now set as httpOnly cookie by the server
// No need to manually set cookie on client side
// Move to next step
if (needsPayment.value) {
currentStep.value = 2;
// Debug log
console.log(
"Customer ID:",
customerId.value,
"Customer Code:",
customerCode.value,
);
// Initialize HelcimPay.js session for card verification
await initializeHelcimPay(customerId.value, customerCode.value, 0);
} else {
// For free tier, create subscription directly
await createSubscription();
// Check member status to ensure user is properly authenticated
await checkMemberStatus();
// Automatically redirect to dashboard after a short delay
setTimeout(() => {
navigateTo("/member/dashboard");
}, 3000); // 3 second delay to show success message
}
}
} catch (error) {
console.error("Error creating customer:", error);
errorMessage.value =
error.data?.message || "Failed to create account. Please try again.";
} finally {
isSubmitting.value = false;
}
};
// Step 2: Process payment
const processPayment = async () => {
if (isSubmitting.value) return;
console.log("Starting payment process...");
isSubmitting.value = true;
errorMessage.value = "";
try {
console.log("Calling verifyPayment()...");
// Verify payment through HelcimPay.js
const paymentResult = await verifyPayment();
console.log("Payment result from HelcimPay:", paymentResult);
if (paymentResult.success) {
paymentToken.value = paymentResult.cardToken;
console.log("Payment successful, cardToken:", paymentResult.cardToken);
console.log("Calling verify-payment endpoint...");
// Verify payment on server
const verifyResult = await $fetch("/api/helcim/verify-payment", {
method: "POST",
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
});
console.log("Payment verification result:", verifyResult);
console.log("Calling createSubscription...");
// Create subscription (don't let subscription errors prevent form progression)
const subscriptionResult = await createSubscription(
paymentResult.cardToken,
);
if (!subscriptionResult || !subscriptionResult.success) {
console.warn(
"Subscription creation failed but payment succeeded:",
subscriptionResult?.error,
);
// Still progress to success page since payment worked
currentStep.value = 3;
successMessage.value =
"Payment successful! Subscription setup may need manual completion.";
}
}
} catch (error) {
console.error("Payment process error:", error);
console.error("Error details:", {
message: error.message,
statusCode: error.statusCode,
statusMessage: error.statusMessage,
data: error.data,
});
errorMessage.value =
error.message || "Payment verification failed. Please try again.";
} finally {
isSubmitting.value = false;
}
};
// Create subscription
const createSubscription = async (cardToken = null) => {
try {
console.log("Creating subscription with:", {
customerId: customerId.value,
contributionTier: form.contributionTier,
cardToken: cardToken ? "present" : "null",
});
const response = await $fetch("/api/helcim/subscription", {
method: "POST",
body: {
customerId: customerId.value,
customerCode: customerCode.value,
contributionTier: form.contributionTier,
cardToken: cardToken,
},
});
console.log("Subscription creation response:", response);
if (response.success) {
subscriptionData.value = response.subscription;
console.log("Moving to step 3 - success!");
currentStep.value = 3;
successMessage.value = "Your membership has been activated successfully!";
// Check member status to ensure user is properly authenticated
await checkMemberStatus();
// Automatically redirect to dashboard after a short delay
setTimeout(() => {
navigateTo("/member/dashboard");
}, 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",
};
}
};
// Go back to previous step
const goBack = () => {
if (currentStep.value > 1) {
currentStep.value--;
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>