Radio-pair cadence selector (Monthly / Annual) added to the join form, reusing the existing .circle-radio styling. contributionItems computed reactively; all tier labels and the left-column price list update on toggle. cadence submitted with the subscription payload. payment-setup hardcoded to monthly (annual upgrades go through /join).
188 lines
5.2 KiB
Vue
188 lines
5.2 KiB
Vue
<template>
|
|
<PageShell>
|
|
<ClientOnly>
|
|
<PageHeader
|
|
title="Set Up Payment"
|
|
:subtitle="targetTier ? `Upgrading to $${targetTier}/month` : 'Payment setup'"
|
|
/>
|
|
|
|
<PageSection>
|
|
<div v-if="step === 'loading'" class="status-block">
|
|
<p>Preparing payment setup…</p>
|
|
</div>
|
|
|
|
<div v-else-if="step === 'error'" class="status-block">
|
|
<div class="error-box">{{ errorMessage }}</div>
|
|
<div class="button-row">
|
|
<button class="btn" @click="initialize">Try again</button>
|
|
<NuxtLink to="/member/account" class="btn">Back to account</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="step === 'ready'" class="status-block">
|
|
<p>
|
|
To upgrade to <strong>${{ targetTier }}/month</strong>, we need a
|
|
payment method on file. Click below to open the secure payment
|
|
form — we'll verify your card with a $0 authorization and then
|
|
activate your new tier.
|
|
</p>
|
|
<div v-if="errorMessage" class="error-box">{{ errorMessage }}</div>
|
|
<div class="button-row">
|
|
<button
|
|
class="btn btn-primary"
|
|
:disabled="isProcessing"
|
|
@click="openModal"
|
|
>
|
|
{{ isProcessing ? 'Processing…' : 'Enter payment details' }}
|
|
</button>
|
|
<NuxtLink to="/member/account" class="btn">Cancel</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="step === 'success'" class="status-block">
|
|
<p>Payment setup complete. Redirecting to your account…</p>
|
|
</div>
|
|
</PageSection>
|
|
</ClientOnly>
|
|
</PageShell>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({ middleware: 'auth' });
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const toast = useToast();
|
|
const { memberData, checkMemberStatus } = useAuth();
|
|
const { initializeHelcimPay, verifyPayment, cleanup: cleanupHelcim } = useHelcimPay();
|
|
|
|
const VALID_TIERS = ['5', '15', '30', '50'];
|
|
const VALID_CIRCLES = ['community', 'founder', 'practitioner'];
|
|
|
|
const targetTier = computed(() => {
|
|
const t = String(route.query.tier || '');
|
|
return VALID_TIERS.includes(t) ? t : null;
|
|
});
|
|
const targetCircle = computed(() => {
|
|
const c = String(route.query.circle || '');
|
|
return VALID_CIRCLES.includes(c) ? c : null;
|
|
});
|
|
|
|
const step = ref('loading'); // loading | ready | success | error
|
|
const errorMessage = ref('');
|
|
const isProcessing = ref(false);
|
|
const customerId = ref('');
|
|
const customerCode = ref('');
|
|
|
|
const initialize = async () => {
|
|
errorMessage.value = '';
|
|
step.value = 'loading';
|
|
|
|
if (!targetTier.value) {
|
|
errorMessage.value = 'Missing or invalid target tier.';
|
|
step.value = 'error';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const customer = await $fetch('/api/helcim/get-or-create-customer', {
|
|
method: 'POST',
|
|
});
|
|
customerId.value = customer.customerId;
|
|
customerCode.value = customer.customerCode;
|
|
|
|
await initializeHelcimPay(customerId.value, customerCode.value, 0);
|
|
step.value = 'ready';
|
|
} catch (err) {
|
|
console.error('Payment setup init failed:', err);
|
|
errorMessage.value =
|
|
err.data?.statusMessage || err.message || 'Failed to initialize payment.';
|
|
step.value = 'error';
|
|
}
|
|
};
|
|
|
|
const openModal = async () => {
|
|
if (isProcessing.value) return;
|
|
isProcessing.value = true;
|
|
errorMessage.value = '';
|
|
|
|
try {
|
|
const result = await verifyPayment();
|
|
if (!result?.success) throw new Error('Payment was not completed.');
|
|
|
|
await $fetch('/api/helcim/verify-payment', {
|
|
method: 'POST',
|
|
body: {
|
|
cardToken: result.cardToken,
|
|
customerId: customerId.value,
|
|
},
|
|
});
|
|
|
|
// Update circle first if it changed — update-contribution only touches tier.
|
|
if (targetCircle.value && targetCircle.value !== memberData.value?.circle) {
|
|
await $fetch('/api/members/update-circle', {
|
|
method: 'POST',
|
|
body: { circle: targetCircle.value },
|
|
});
|
|
}
|
|
|
|
await $fetch('/api/members/update-contribution', {
|
|
method: 'POST',
|
|
// cadence: annual upgrades go through /join; this page is monthly-only
|
|
body: { contributionTier: targetTier.value, cadence: 'monthly' },
|
|
});
|
|
|
|
await checkMemberStatus();
|
|
step.value = 'success';
|
|
toast.add({ title: 'Payment method saved', color: 'success' });
|
|
setTimeout(() => router.push('/member/account'), 1500);
|
|
} catch (err) {
|
|
console.error('Payment setup error:', err);
|
|
errorMessage.value =
|
|
err.data?.statusMessage || err.message || 'Payment setup failed.';
|
|
// Re-initialize Helcim session so the user can try again.
|
|
cleanupHelcim();
|
|
await initialize();
|
|
} finally {
|
|
isProcessing.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
initialize();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
cleanupHelcim();
|
|
});
|
|
|
|
useHead({ title: 'Set Up Payment - Ghost Guild' });
|
|
</script>
|
|
|
|
<style scoped>
|
|
.status-block {
|
|
padding: 12px 0;
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
color: var(--text);
|
|
}
|
|
|
|
.status-block p {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.error-box {
|
|
padding: 12px 14px;
|
|
border: 1px dashed var(--ember);
|
|
color: var(--ember);
|
|
font-size: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.button-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
</style>
|