Skip to content

Express Checkout Element

The Express Checkout Element displays one-click payment buttons for Apple Pay, Google Pay, and Link. It provides the fastest checkout experience for customers with saved payment methods.

Basic Setup

vue
<script setup>
import { ref } from 'vue'
import {
  StripeProvider,
  StripeElements,
  StripeExpressCheckoutElement,
  useStripe
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('pi_..._secret_...')

const handleConfirm = async (event) => {
  const { stripe } = useStripe()

  const { error } = await stripe.value.confirmPayment({
    elements: event.elements,
    confirmParams: {
      return_url: `${window.location.origin}/complete`
    }
  })

  if (error) {
    console.error(error.message)
  }
}
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <VueStripeElements :client-secret="clientSecret">
      <VueStripeExpressCheckoutElement @confirm="handleConfirm" />
    </VueStripeElements>
  </VueStripeProvider>
</template>

How It Works

The Express Checkout Element:

  1. Detects available payment methods (Apple Pay on Safari/iOS, Google Pay on Chrome/Android)
  2. Shows only the buttons that are available for the customer
  3. Handles the wallet authentication flow
  4. Returns the payment result for you to confirm

HTTPS Required

Apple Pay and Google Pay only work over HTTPS, even in development. Use a tool like ngrok or localhost.run for local testing.

Handling the Click Event

Use @click to customize the payment sheet:

vue
<VueStripeExpressCheckoutElement
  @click="handleClick"
  @confirm="handleConfirm"
/>

<script setup>
const handleClick = ({ resolve }) => {
  // Customize what appears in the payment sheet
  resolve({
    // Business name shown in the sheet
    business: { name: 'My Store' },
    // Show/hide email field
    emailRequired: true,
    // Show/hide phone field
    phoneNumberRequired: false,
    // Enable shipping address collection
    shippingAddressRequired: true,
    // Allowed shipping countries
    allowedShippingCountries: ['US', 'CA', 'GB'],
    // Shipping options
    shippingRates: [
      {
        id: 'standard',
        displayName: 'Standard Shipping',
        amount: 500 // $5.00
      },
      {
        id: 'express',
        displayName: 'Express Shipping',
        amount: 1500 // $15.00
      }
    ]
  })
}
</script>

Dynamic Shipping Rates

Update shipping rates when the address changes:

vue
<script setup>
const handleShippingAddressChange = async ({ address, resolve }) => {
  // Fetch shipping rates for this address from your backend
  const rates = await fetchShippingRates(address)

  resolve({
    shippingRates: rates.map(rate => ({
      id: rate.id,
      displayName: rate.name,
      amount: rate.price,
      deliveryEstimate: {
        minimum: { unit: 'day', value: rate.minDays },
        maximum: { unit: 'day', value: rate.maxDays }
      }
    }))
  })
}
</script>

<template>
  <VueStripeExpressCheckoutElement
    @click="handleClick"
    @confirm="handleConfirm"
    @shippingaddresschange="handleShippingAddressChange"
  />
</template>

Combining with Payment Element

A common pattern is to show Express Checkout above the Payment Element:

vue
<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <VueStripeElements :client-secret="clientSecret">
      <div class="checkout-form">
        <!-- Express Checkout at top -->
        <VueStripeExpressCheckoutElement
          @confirm="handleExpressConfirm"
          @ready="handleExpressReady"
        />

        <!-- Divider (only show if express checkout is available) -->
        <div v-if="hasExpressCheckout" class="divider">
          <span>Or pay with card</span>
        </div>

        <!-- Payment Element below -->
        <form @submit.prevent="handleSubmit">
          <VueStripePaymentElement />
          <button type="submit">Pay $20.00</button>
        </form>
      </div>
    </VueStripeElements>
  </VueStripeProvider>
</template>

<script setup>
const hasExpressCheckout = ref(false)

const handleExpressReady = ({ availablePaymentMethods }) => {
  // Check if any express payment methods are available
  hasExpressCheckout.value =
    availablePaymentMethods?.applePay ||
    availablePaymentMethods?.googlePay ||
    availablePaymentMethods?.link
}
</script>

<style scoped>
.divider {
  display: flex;
  align-items: center;
  text-align: center;
  margin: 24px 0;
}

.divider::before,
.divider::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid #e6e6e6;
}

.divider span {
  padding: 0 16px;
  color: #6b7280;
  font-size: 14px;
}
</style>

Complete Example

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import {
  StripeProvider,
  StripeElements,
  StripeExpressCheckoutElement,
  StripePaymentElement,
  useStripe,
  useStripeElements
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('')
const hasExpressCheckout = ref(false)
const processing = ref(false)
const errorMessage = ref('')

onMounted(async () => {
  const response = await fetch('/api/create-payment-intent', {
    method: 'POST',
    body: JSON.stringify({ amount: 2000 })
  })
  const data = await response.json()
  clientSecret.value = data.clientSecret
})

const handleExpressReady = ({ availablePaymentMethods }) => {
  hasExpressCheckout.value =
    availablePaymentMethods?.applePay ||
    availablePaymentMethods?.googlePay
}

const handleClick = ({ resolve }) => {
  resolve({
    business: { name: 'My Store' },
    shippingAddressRequired: true,
    shippingRates: [
      { id: 'free', displayName: 'Free Shipping', amount: 0 },
      { id: 'express', displayName: 'Express', amount: 1000 }
    ]
  })
}

const handleExpressConfirm = async (event) => {
  const { stripe } = useStripe()

  const { error } = await stripe.value.confirmPayment({
    elements: event.elements,
    confirmParams: {
      return_url: `${window.location.origin}/complete`
    }
  })

  if (error) {
    errorMessage.value = error.message || 'Payment failed'
  }
}

const handleSubmit = async () => {
  const { stripe } = useStripe()
  const { elements } = useStripeElements()

  if (!stripe.value || !elements.value) return

  processing.value = true

  const { error } = await stripe.value.confirmPayment({
    elements: elements.value,
    confirmParams: {
      return_url: `${window.location.origin}/complete`
    }
  })

  if (error) {
    errorMessage.value = error.message || 'Payment failed'
    processing.value = false
  }
}
</script>

<template>
  <div class="checkout">
    <VueStripeProvider :publishable-key="publishableKey">
      <VueStripeElements v-if="clientSecret" :client-secret="clientSecret">
        <!-- Express Checkout -->
        <VueStripeExpressCheckoutElement
          @ready="handleExpressReady"
          @click="handleClick"
          @confirm="handleExpressConfirm"
        />

        <!-- Divider -->
        <div v-if="hasExpressCheckout" class="divider">
          <span>Or pay with card</span>
        </div>

        <!-- Regular Payment Form -->
        <form @submit.prevent="handleSubmit">
          <VueStripePaymentElement />

          <div v-if="errorMessage" class="error">
            {{ errorMessage }}
          </div>

          <button type="submit" :disabled="processing">
            {{ processing ? 'Processing...' : 'Pay $20.00' }}
          </button>
        </form>
      </VueStripeElements>

      <div v-else>Loading...</div>
    </VueStripeProvider>
  </div>
</template>

Testing

Apple Pay

  • Use Safari on macOS or iOS
  • Must have a card in Apple Wallet
  • Use test mode cards: Any card will work in test mode

Google Pay

  • Use Chrome
  • Must have a card saved in Google Pay
  • Works in test mode with any saved card
  • Works in any browser
  • Uses Stripe's Link service
  • Auto-fills for returning customers

Next Steps

Released under the MIT License.