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

View File

@@ -22,7 +22,11 @@ export const useBrandingStore = defineStore(
watch( watch(
() => route.params.creator, () => route.params.creator,
async (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(); const { t } = useI18n();
function goBack() { function goBack() {
const returnUrl = route.query.returnUrl; // Navigate back to the creator's page
if (returnUrl) { const creatorName = route.params.creator?.split('/')[0] || '';
router.push(returnUrl); router.push(`/@${creatorName}`);
} else {
router.back();
}
} }
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,47 +2,24 @@
<v-dialog v-model="donationModal"> <v-dialog v-model="donationModal">
<DonationForm <DonationForm
:show-cancel-button="showCancelButton" :show-cancel-button="showCancelButton"
:creator-id="creatorId"
:creator-name="creatorName"
:on-success-url="onSuccessUrl"
:on-cancelled-url="onCancelledUrl"
@cancel="closeDonationDialog" @cancel="closeDonationDialog"
@submit="handleSubmit"
/> />
</v-dialog> </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> </template>
<script setup> <script setup>
import {useClient} from '@/plugins/api.js'; import {ref} from 'vue';
import {loadStripe} from '@stripe/stripe-js';
import {onMounted, ref} from 'vue';
import { useI18n } from 'vue-i18n';
import DonationForm from './DonationForm.vue'; import DonationForm from './DonationForm.vue';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
creatorId: {default: 'missing-creator-id', required: true}, creatorId: {default: 'missing-creator-id', required: true},
creatorName: {default: 'missing-creator-name', required: true}, creatorName: {default: 'missing-creator-name', required: true},
onSuccessUrl: {default: 'missing-on-success-u', required: true}, onSuccessUrl: {default: 'missing-on-success-u', required: true},
onCancelledUrl: {default: 'missing-on-cancelled-url', required: true}, onCancelledUrl: {default: 'missing-on-cancelled-url', required: true},
iconColorClass: {default: 'text-black'},
showCancelButton: { showCancelButton: {
type: Boolean, type: Boolean,
default: true default: true
@@ -51,16 +28,7 @@ const props = defineProps({
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const errorMessage = ref('');
const donationModal = ref(false); 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() { function openDonationDialog() {
donationModal.value = true; donationModal.value = true;
@@ -71,101 +39,7 @@ function closeDonationDialog() {
emit('close'); 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({ defineExpose({
openDonationDialog openDonationDialog
}); });
</script> </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 clearable
auto-grow auto-grow
></v-textarea> ></v-textarea>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
</div> </div>
<div class="card-actions"> <div class="card-actions">
@@ -43,8 +45,10 @@
</button> </button>
<button class="primary" <button class="primary"
@click="$emit('submit', { amount: tipAmountInDollars, message: tipMessage })"> @click="handleSubmit"
{{ t('creator.donation.send') }} :disabled="isProcessing">
<span v-if="isProcessing" class="spinner mr-2"></span>
{{ isProcessing ? t('creator.donation.processing') : t('creator.donation.send') }}
</button> </button>
</div> </div>
</div> </div>
@@ -53,13 +57,27 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
const { t } = useI18n(); const { t } = useI18n();
const client = useClient();
const props = defineProps({ const props = defineProps({
showCancelButton: { showCancelButton: {
type: Boolean, type: Boolean,
default: true 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 tipAmountInDollars = ref('');
const tipMessage = ref(''); const tipMessage = ref('');
const errorMessage = ref('');
const isProcessing = ref(false);
function preventNonNumeric(event) { function preventNonNumeric(event) {
const key = event.key; const key = event.key;
@@ -76,8 +96,57 @@ function preventNonNumeric(event) {
event.preventDefault(); 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> </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> <i18n>
{ {
"en": { "en": {
@@ -89,7 +158,12 @@ function preventNonNumeric(event) {
"isupport": "I Support", "isupport": "I Support",
"amount": "Amount ($)", "amount": "Amount ($)",
"message": "Message (optional)", "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", "isupport": "Je Soutiens",
"amount": "Montant ($)", "amount": "Montant ($)",
"message": "Message (optionnel)", "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", "isupport": "Apoyo",
"amount": "Cantidad ($)", "amount": "Cantidad ($)",
"message": "Mensaje (opcional)", "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"
}
} }
} }
} }