Appearance
Vue Example
A checkout component using Vue 3 Composition API with Stripe Elements.
Checkout Component
vue
<script setup>
import { ref, onMounted } from 'vue';
import { loadStripe } from '@stripe/stripe-js';
const props = defineProps({
orderTotal: { type: Number, required: true }, // in cents
organizationId: { type: String, required: true },
apiToken: { type: String, required: true },
});
const emit = defineEmits(['payment-success', 'payment-error']);
const loading = ref(false);
const error = ref(null);
const paymentReady = ref(false);
let stripe = null;
let elements = null;
let stripeAccountId = null;
const BACKEND_URL = '/api';
onMounted(async () => {
await initializeStripe();
});
async function initializeStripe() {
try {
// 1. Fetch Stripe config
const configResponse = await fetch(`${BACKEND_URL}/stripe-config`, {
headers: { 'Authorization': `Bearer ${props.apiToken}` },
});
const config = await configResponse.json();
// 2. Initialize Stripe.js
stripe = await loadStripe(config.publishable_key);
stripeAccountId = config.stripe_account_id || null;
// 3. Mount Payment Element
mountPaymentElement();
} catch (err) {
error.value = 'Failed to initialize payment form.';
}
}
function mountPaymentElement() {
const elementsOptions = {
mode: 'payment',
amount: props.orderTotal,
currency: 'usd',
appearance: { theme: 'stripe' },
};
if (stripeAccountId) {
elementsOptions.on_behalf_of = stripeAccountId;
}
elements = stripe.elements(elementsOptions);
const paymentElement = elements.create('payment', { layout: 'tabs' });
paymentElement.mount('#payment-element');
paymentElement.on('ready', () => {
paymentReady.value = true;
});
}
async function handleSubmit() {
if (loading.value) return;
loading.value = true;
error.value = null;
try {
// 1. Validate Payment Element
const { error: submitError } = await elements.submit();
if (submitError) {
error.value = submitError.message;
return;
}
// 2. Create PaymentIntent
const intentResponse = await fetch(`${BACKEND_URL}/payment-intent`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${props.apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: props.orderTotal,
organization_id: props.organizationId,
}),
});
if (!intentResponse.ok) {
const errorData = await intentResponse.json();
error.value = errorData.error || 'Failed to create payment.';
return;
}
const { client_secret, payment_intent_id } = await intentResponse.json();
// 3. Confirm with Stripe
const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
elements,
clientSecret: client_secret,
confirmParams: {
return_url: window.location.href,
},
redirect: 'if_required',
});
if (confirmError) {
error.value = confirmError.message;
return;
}
if (paymentIntent.status === 'succeeded') {
emit('payment-success', {
paymentIntentId: payment_intent_id,
stripePaymentIntent: paymentIntent,
});
}
} catch (err) {
error.value = 'An unexpected error occurred.';
emit('payment-error', err);
} finally {
loading.value = false;
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div id="payment-element">
<!-- Stripe Payment Element mounts here -->
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<button
type="submit"
:disabled="loading || !paymentReady"
>
<span v-if="loading">Processing...</span>
<span v-else>Pay ${{ (orderTotal / 100).toFixed(2) }}</span>
</button>
</form>
</template>
<style scoped>
form {
max-width: 500px;
margin: 0 auto;
}
#payment-element {
margin-bottom: 1.5rem;
}
.error-message {
color: #df1b41;
margin-bottom: 1rem;
font-size: 0.875rem;
}
button {
width: 100%;
padding: 0.75rem 1rem;
background: #5469d4;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background: #4358b5;
}
</style>Using the Component
vue
<script setup>
import CheckoutPayment from './CheckoutPayment.vue';
const orderTotal = 3420; // $34.20 in cents
const organizationId = 'org_00000k1L2m3N4o5';
async function onPaymentSuccess({ paymentIntentId }) {
// Submit the order with the successful payment
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_intent_id: paymentIntentId,
items: cart.items,
}),
});
if (response.ok) {
router.push('/order/confirmation');
}
}
function onPaymentError(err) {
console.error('Payment failed:', err);
}
</script>
<template>
<div class="checkout-page">
<h1>Checkout</h1>
<CheckoutPayment
:order-total="orderTotal"
:organization-id="organizationId"
:api-token="authToken"
@payment-success="onPaymentSuccess"
@payment-error="onPaymentError"
/>
</div>
</template>Cancelling on Order Failure
If order creation fails after a successful payment, cancel the payment intent:
javascript
async function onPaymentSuccess({ paymentIntentId }) {
try {
await submitOrder(paymentIntentId);
router.push('/order/confirmation');
} catch (err) {
// Cancel the payment intent to release the customer's funds
await fetch(`/api/payment-intent/${paymentIntentId}/cancel`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authToken}` },
});
error.value = 'Order could not be placed. Your payment has been cancelled.';
}
}Changelog
| Date | Change |
|---|---|
| 2026-03-19 | Initial publication. |