feat: Add new payment completion and failure routes, update donation handling in DonationForm and related components

This commit is contained in:
2025-04-24 13:55:26 -04:00
parent af80f30f7a
commit 3a6ee307cd
9 changed files with 120 additions and 164 deletions

View File

@@ -35,6 +35,16 @@ const routes = [
path: '',
name: 'creator',
component: CreatorHome,
},
{
path: 'tip-completed',
name: 'PaymentCompleted',
component: PaymentCompleted,
},
{
path: 'tip-cancelled',
name: 'PaymentFailed',
component: PaymentFailed,
}
],
},
@@ -85,16 +95,6 @@ const routes = [
component: LoginView,
meta: { notAuthenticated: true },
},
{
path: '/paymentcompleted/:creatorId',
name: 'PaymentCompleted',
component: PaymentCompleted,
},
{
path: '/paymentfailed/:creatorId',
name: 'PaymentFailed',
component: PaymentFailed,
},
{
path: '/profile',
name: 'profile',

View File

@@ -22,7 +22,11 @@ export const useBrandingStore = defineStore(
watch(
() => route.params.creator,
async (creator) => {
await updateBrand(creator);
console.log(`creator: ${creator}`)
// Extract just the creator name from the path (remove any additional segments)
const creatorName = creator ? creator.split('/')[0] : undefined;
console.log(`name: ${creatorName}`)
await updateBrand(creatorName);
}
)

View File

@@ -52,12 +52,9 @@ const route = useRoute();
const { t } = useI18n();
function goBack() {
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.push(returnUrl);
} else {
router.back();
}
// Navigate back to the creator's page
const creatorName = route.params.creator?.split('/')[0] || '';
router.push(`/@${creatorName}`);
}
</script>

View File

@@ -21,12 +21,8 @@ const route = useRoute();
const { t } = useI18n();
function goBack() {
const returnUrl = route.query.returnUrl;
if (returnUrl) {
router.push(returnUrl);
} else {
router.back();
}
const creatorName = route.params.creator?.split('/')[0] || '';
router.push(`/@${creatorName}`);
}
</script>

View File

@@ -42,8 +42,8 @@ const {t} = useI18n();
v-if="brandingStore.value?.acceptDonation"
:creator-id="brandingStore.value?.id"
:creator-name="brandingStore.value?.name"
:on-cancelled-url="baseURL + '/paymentfailed/' + brandingStore.value?.id"
:on-success-url="baseURL + '/paymentcompleted/' + brandingStore.value?.id"
:on-cancelled-url="baseURL + '/@' + brandingStore.value.slug + '/tip-cancelled'"
:on-success-url="baseURL + '/@' + brandingStore.value.slug + '/tip-completed'"
/>
</div>
</div>

View File

@@ -7,10 +7,11 @@ import ActualBanner from "@/views/creators/ActualBanner.vue";
import BannerActions from "@/views/creators/BannerActions.vue";
const brandingStore = useBrandingStore();
const creatorName = window.location.pathname.split('/@').pop();
const creatorName = window.location.pathname.split('/@')[1]?.split('/')[0] || '';
const { t } = useI18n();
onMounted(async () => {
console.log(`creatorName: ${creatorName}`)
await brandingStore.updateBrand(creatorName);
});

View File

@@ -7,7 +7,7 @@
{{ t('creator.donation.isupport') }}
</button>
<donation-dialog
<DonationDialog
ref="donationDialogRef"
:creator-id="creatorId"
:creator-name="creatorName"

View File

@@ -2,47 +2,24 @@
<v-dialog v-model="donationModal">
<DonationForm
:show-cancel-button="showCancelButton"
:creator-id="creatorId"
:creator-name="creatorName"
:on-success-url="onSuccessUrl"
:on-cancelled-url="onCancelledUrl"
@cancel="closeDonationDialog"
@submit="handleSubmit"
/>
</v-dialog>
<v-dialog v-model="isPaymentDialogActive" max-width="720" persistent>
<template v-slot:default>
<v-card :style="{ padding: '20px' }">
<div id="checkout"></div>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
<v-spacer></v-spacer>
<v-card-actions>
<v-btn
block
class="ma-auto"
@click="closeDialog()"
>{{ t('common.cancel') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script setup>
import {useClient} from '@/plugins/api.js';
import {loadStripe} from '@stripe/stripe-js';
import {onMounted, ref} from 'vue';
import { useI18n } from 'vue-i18n';
import {ref} from 'vue';
import DonationForm from './DonationForm.vue';
const { t } = useI18n();
const props = defineProps({
creatorId: {default: 'missing-creator-id', required: true},
creatorName: {default: 'missing-creator-name', required: true},
onSuccessUrl: {default: 'missing-on-success-u', required: true},
onCancelledUrl: {default: 'missing-on-cancelled-url', required: true},
iconColorClass: {default: 'text-black'},
showCancelButton: {
type: Boolean,
default: true
@@ -51,16 +28,7 @@ const props = defineProps({
const emit = defineEmits(['close']);
const errorMessage = ref('');
const donationModal = ref(false);
const isPaymentDialogActive = ref(false);
let stripe = null;
let checkout;
onMounted(async () => {
stripe = await loadStripe(import.meta.env.VITE_STRIPE_API_KEY);
});
function openDonationDialog() {
donationModal.value = true;
@@ -71,101 +39,7 @@ function closeDonationDialog() {
emit('close');
}
async function createCheckoutSession(amount, message) {
const client = useClient();
try {
let clientSecret = await client.post(`/api/tips`, {
creatorId: props.creatorId,
amount: amount * 100,
currency: 'CAD',
message: message,
checkoutSuccessUrl: props.onSuccessUrl,
checkoutCancelledUrl: props.onCancelledUrl,
});
return clientSecret.data;
} catch (error) {
console.error(error);
errorMessage.value = t('creator.donation.errors.payment');
}
}
function closeDialog() {
isPaymentDialogActive.value = false;
errorMessage.value = '';
if (checkout) {
checkout.destroy();
}
}
async function handleSubmit({ amount, message }) {
isPaymentDialogActive.value = true;
const response = await createCheckoutSession(amount, message);
if (response && response.url) {
// Redirect to the Stripe Checkout page
window.location.href = response.url;
} else {
errorMessage.value = t('creator.donation.errors.payment');
isPaymentDialogActive.value = false;
}
}
defineExpose({
openDonationDialog
});
</script>
<style scoped>
.error-message {
color: white;
background-color: red;
border-radius: 4px;
text-align: center;
width: 100%;
padding: 5px;
}
</style>
<i18n>
{
"en": {
"common": {
"cancel": "Cancel"
},
"creator": {
"donation": {
"errors": {
"payment": "An error occurred during payment processing"
}
}
}
},
"fr": {
"common": {
"cancel": "Annuler"
},
"creator": {
"donation": {
"errors": {
"payment": "Une erreur s'est produite lors du traitement du paiement"
}
}
}
},
"es": {
"common": {
"cancel": "Cancelar"
},
"creator": {
"donation": {
"errors": {
"payment": "Ocurrió un error durante el procesamiento del pago"
}
}
}
}
}
</i18n>

View File

@@ -33,6 +33,8 @@
clearable
auto-grow
></v-textarea>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
</div>
<div class="card-actions">
@@ -43,8 +45,10 @@
</button>
<button class="primary"
@click="$emit('submit', { amount: tipAmountInDollars, message: tipMessage })">
{{ t('creator.donation.send') }}
@click="handleSubmit"
:disabled="isProcessing">
<span v-if="isProcessing" class="spinner mr-2"></span>
{{ isProcessing ? t('creator.donation.processing') : t('creator.donation.send') }}
</button>
</div>
</div>
@@ -53,13 +57,27 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
const { t } = useI18n();
const client = useClient();
const props = defineProps({
showCancelButton: {
type: Boolean,
default: true
},
creatorId: {
type: String,
required: true
},
onSuccessUrl: {
type: String,
required: true
},
onCancelledUrl: {
type: String,
required: true
}
});
@@ -67,6 +85,8 @@ const emit = defineEmits(['cancel', 'submit']);
const tipAmountInDollars = ref('');
const tipMessage = ref('');
const errorMessage = ref('');
const isProcessing = ref(false);
function preventNonNumeric(event) {
const key = event.key;
@@ -76,8 +96,57 @@ function preventNonNumeric(event) {
event.preventDefault();
}
}
async function handleSubmit() {
if (!tipAmountInDollars.value || tipAmountInDollars.value <= 0) {
errorMessage.value = t('creator.donation.errors.invalidAmount');
return;
}
isProcessing.value = true;
errorMessage.value = '';
try {
const response = await client.post(`/api/tips`, {
creatorId: props.creatorId,
amount: tipAmountInDollars.value * 100,
currency: 'CAD',
message: tipMessage.value,
checkoutSuccessUrl: props.onSuccessUrl,
checkoutCancelledUrl: props.onCancelledUrl,
});
if (response.data?.stripeCheckoutUrl) {
window.location.href = response.data.stripeCheckoutUrl;
} else {
throw new Error('No checkout URL received');
}
} catch (error) {
console.error(error);
errorMessage.value = t('creator.donation.errors.payment');
isProcessing.value = false;
}
}
</script>
<style scoped>
.error-message {
@apply text-white bg-red-500;
@apply rounded-md text-center w-full p-2;
@apply mt-2;
}
.spinner {
@apply inline-block;
@apply w-4 h-4;
@apply border-2;
@apply border-current;
@apply border-t-transparent;
@apply rounded-full;
@apply animate-spin;
}
</style>
<i18n>
{
"en": {
@@ -89,7 +158,12 @@ function preventNonNumeric(event) {
"isupport": "I Support",
"amount": "Amount ($)",
"message": "Message (optional)",
"send": "Send"
"send": "Send",
"processing": "Processing...",
"errors": {
"payment": "An error occurred during payment processing",
"invalidAmount": "Please enter a valid amount"
}
}
}
},
@@ -102,7 +176,12 @@ function preventNonNumeric(event) {
"isupport": "Je Soutiens",
"amount": "Montant ($)",
"message": "Message (optionnel)",
"send": "Envoyer"
"send": "Envoyer",
"processing": "Traitement en cours...",
"errors": {
"payment": "Une erreur s'est produite lors du traitement du paiement",
"invalidAmount": "Veuillez entrer un montant valide"
}
}
}
},
@@ -115,7 +194,12 @@ function preventNonNumeric(event) {
"isupport": "Apoyo",
"amount": "Cantidad ($)",
"message": "Mensaje (opcional)",
"send": "Enviar"
"send": "Enviar",
"processing": "Procesando...",
"errors": {
"payment": "Ocurrió un error durante el procesamiento del pago",
"invalidAmount": "Por favor ingrese un monto válido"
}
}
}
}