Skip to content

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
DateChange
2026-03-19Initial publication.

ShopHero CommerceCore Platform