ghostguild-org/app/pages/join.vue

655 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- Page Header - Context aware -->
<PageHeader
v-if="!isAuthenticated"
title="Join Ghost Guild"
subtitle=""
theme="gray"
size="large"
/>
<PageHeader
v-else
title="You're Already a Member!"
:subtitle="`Welcome back, ${memberData?.name || 'member'}. You're already part of Ghost Guild in the ${memberData?.circle || 'community'} circle.`"
theme="gray"
size="large"
/>
<!-- How Ghost Guild Works -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-2xl">
<h2 class="text-3xl font-bold text-[--ui-text] mb-6">
How Membership Works
</h2>
<p class="text-lg text-[--ui-text] mb-4">
Every member gets full access to our resource library, workshops,
events, Slack community, and peer support. Your circle connects you
with other folks and resources for your stage.
</p>
<p class="text-lg text-[--ui-text]">
Contribute what you can afford ($050+/month). Higher contributions
create solidarity spots for those who need them. You can adjust
anytime.
</p>
</div>
</UContainer>
</section>
<!-- Membership Sign Up Form -->
<section v-if="!isAuthenticated" 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>
</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-primary bg-primary/5'
: 'border-neutral-200 hover:border-neutral-400'
"
>
<input
v-model="form.circle"
type="radio"
:value="option.value"
name="circle"
class="sr-only"
/>
<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>
<!-- Member Info Section - Shows for logged-in members -->
<section v-if="isAuthenticated" class="py-20 bg-[--ui-bg]">
<UContainer class="max-w-4xl">
<div class="bg-[--ui-bg-elevated] rounded-xl p-8 mb-8">
<h2 class="text-2xl font-bold text-[--ui-text] mb-6">
Your Membership
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-[--ui-bg] rounded-lg p-6">
<h3 class="text-sm font-medium text-[--ui-text-muted] mb-2">
Circle
</h3>
<p class="text-xl font-semibold text-[--ui-text] capitalize">
{{ memberData?.circle || "Community" }}
</p>
</div>
<div class="bg-[--ui-bg] rounded-lg p-6">
<h3 class="text-sm font-medium text-[--ui-text-muted] mb-2">
Contribution
</h3>
<p class="text-xl font-semibold text-[--ui-text]">
${{ memberData?.contributionTier || "0" }} CAD/month
</p>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<UButton to="/member/dashboard" size="lg">
Go to Dashboard
</UButton>
<UButton to="/member/profile" variant="outline" size="lg">
Edit Profile
</UButton>
</div>
</div>
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-6">
<h3 class="text-lg font-semibold text-[--ui-text] mb-3">
Want to change your circle or contribution?
</h3>
<p class="text-[--ui-text] mb-4">
You can update your circle and adjust your monthly contribution at
any time from your profile settings.
</p>
<UButton to="/member/profile" variant="soft" color="primary">
Update Membership Settings
</UButton>
</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";
// 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",
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();
// 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) {
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;
// 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 welcome page after a short delay
setTimeout(() => {
navigateTo("/welcome");
}, 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;
isSubmitting.value = true;
errorMessage.value = "";
try {
// Verify payment through HelcimPay.js
const paymentResult = await verifyPayment();
if (paymentResult.success) {
paymentToken.value = paymentResult.cardToken;
// Verify payment on server
const verifyResult = await $fetch("/api/helcim/verify-payment", {
method: "POST",
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value,
},
});
// 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);
errorMessage.value =
error.message || "Payment verification failed. Please try again.";
} 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,
cardToken: cardToken,
},
});
if (response.success) {
subscriptionData.value = response.subscription;
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 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",
};
}
};
// 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>