svelte-stripe

Everything you need to add Stripe Elements to your Svelte & SvelteKit projects.

Links: npm github changelog license

Installation

To configure your project, add these 2 packages:

pnpm install -D stripe svelte-stripe
  • stripe is the official server-side version of Stripe.
  • svelte-stripe is the community-supported wrapper for Stripe Elements.

Docs

Set up Stripe

Add your private and public keys to your environment:

PUBLIC_STRIPE_KEY=pk_test_...
SECRET_STRIPE_KEY=sk_test_...

In your payment page, initialize Stripe and add a <Elements> component:

<script>
  import { loadStripe } from '@stripe/stripe-js'
  import { Elements } from 'svelte-stripe'
  import { onMount } from 'svelte'
  import { PUBLIC_STRIPE_KEY } from '$env/static/public'

  let stripe = null

  onMount(async () => {
    stripe = await loadStripe(PUBLIC_STRIPE_KEY)
  })
</script>

<Elements {stripe}>
  <!-- this is where your Stripe components go -->
</Elements>

Creating a payment intent

Before making a charge, Stripe should be notified by creating a payment intent. It’s a way to tell Stripe what amount to capture and to attach any relavent metadata, for example, the products they are buying. This must happen server-side to avoid anyone tampering with the amount.

Let’s add an endpoint src/routes/create-payment-intent/+server.js to create the “payment intent”:

import Stripe from 'stripe'
import { SECRET_STRIPE_KEY } from '$env/static/private'

// initialize Stripe
const stripe = new Stripe(SECRET_STRIPE_KEY)

// handle POST /create-payment-intent
export async function POST() {
  // create the payment intent
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 2000,
    // note, for some EU-only payment methods it must be EUR
    currency: 'usd',
    // specify what payment methods are allowed
    // can be card, sepa_debit, ideal, etc...
    payment_method_types: ['card']
  })

  // return the clientSecret to the client
  return {
    body: {
      clientSecret: paymentIntent.client_secret
    }
  }
}

Accepting payments

There are several types of payment you can accept:

Payment Element

An all-in-one component that supports credit cards, SEPA, GooglePay and ApplePay.

To use it, drop a <PaymentElement> component in your form:

<form on:submit|preventDefault="{submit}">
  <Elements {stripe} {clientSecret} bind:elements>
    <PaymentElement options={...} />
  </Elements>

  <button>Pay</button>
</form>

Then when creating the payment intent, enable the automatic_payment_methods: option:

const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000,
  currency: 'eur',
  automatic_payment_methods: {
    enabled: true
  }
})

Once the form is submitted, call stripe.confirmPayment()

const result = await stripe.confirmPayment({
  elements,
  // specify redirect: 'if_required' or a `return_url`
  redirect: 'if_required'
})

code demo

With Link, customer’s don’t have to re-enter payment and address details for each purchase. Their details are retreived based on their e-mail address.

Once they enter their e-mail they receive an SMS code to verify their identity.

It works in conjuction with <PaymentElement>:

<form on:submit|preventDefault="{submit}">
  <Elements {stripe} {clientSecret} bind:elements>
    <LinkAuthenticationElement />
    <PaymentElement />
  </Elements>

  <button>Pay</button>
</form>

code demo

Credit Cards

These use the <CardNumber>, <CardExpiry> and <CardCvc> components:

<Elements {stripe}>
  <form on:submit|preventDefault="{submit}">
    <CardNumber bind:element="{cardElement}" />
    <CardExpiry />
    <CardCvc />

    <button>Pay</button>
  </form>
</Elements>

When the form submits, pass the cardElement to stripe.confirmCardPayment(), ie:

const result = await stripe
  .confirmCardPayment(clientSecret, {
    payment_method: {
      card: cardElement,
      billing_details: {
        ...
      }
    }
  })

code demo

GooglePay & ApplePay

To display a GooglePay or ApplePay button, use the <PaymentRequestButton/>.

<Elements {stripe}>
  <PaymentRequestButton {paymentRequest} on:paymentmethod="{pay}" />
</Elements>

It requires that you pass metadata using the paymentRequest prop:

// declare payment metadata (amounts must match payment intent)
const paymentRequest = {
  country: 'US',
  currency: 'usd',
  total: { label: 'Demo total', amount: 1099 },
  requestPayerName: true,
  requestPayerEmail: true
}

And define an event handler for the on:paymentmethod event:

async function pay(e) {
  const paymentMethod = e.detail.paymentMethod

  let result = await stripe.confirmCardPayment(clientSecret, {
    payment_method: paymentMethod.id
  })

  if (result.error) {
    // mark failed
    e.detail.complete('fail')

    // payment failed, notify user
    error = result.error
  } else {
    // mark succeeded
    e.detail.complete('success')

    // payment succeeded, redirect to "thank you" page
    goto('/thanks')
  }
}

code demo

SEPA

To process SEPA debits, use the <Iban> component:

<Elements {stripe}>
  <form on:submit|preventDefault="{submit}">
    <input name="name" bind:value="{name}" placeholder="Name" />

    <!-- customize the list of countries, or use "SEPA" to allow all supported countries -->
    <Iban supportedCountries={['SEPA']} bind:element={ibanElement}/>

    <button>Pay</button>
  </form>
</Elements>

To process the payment use stripe.confirmSepaDebitPayment():

const result = await stripe.confirmSepaDebitPayment(clientSecret, {
  payment_method: {
    sepa_debit: ibanElement,
    billing_details: {
      name,
      email
    }
  }
})

code demo

iDEAL

To accept iDEAL payments, use the <Ideal> component:

<Elements {stripe}>
  <form on:submit|preventDefault="{submit}">
    <input name="name" bind:value="{name}" placeholder="Name" />
    <input name="email" bind:value="{email}" placeholder="E-mail" type="email" />
    <Ideal bind:element="{idealElement}" />

    <button>Pay</button>
  </form>
</Elements>

To complete the payment call stripe.confirmIdealPayment(), and make sure the pass a return_url:

const result = await stripe.confirmIdealPayment(clientSecret, {
  payment_method: {
    ideal: idealElement,
    billing_details: {
      name,
      email
    }
  },
  return_url: `${window.location.origin}/return`
})

code demo

Webhooks

After the payment succeeds or fails, Stripe will send out a webhook, which can be used to provision or fulfill the purchase.

The webhook payload contains a signature that should be verified to ensure the data originated from Stripe.

Here’s an example of handling a charge.succeeded webhook with SvelteKit:

// in src/routes/stripe/webhooks/+server.js
import Stripe from 'stripe'
import { error, json } from '@sveltejs/kit'
import { env } from '$env/dynamic/private'

// init api client
const stripe = new Stripe(env.SECRET_STRIPE_KEY)

// endpoint to handle incoming webhooks
export async function POST({ request }) {
  // extract body
  const body = await request.text()

  // get the signature from the header
  const signature = request.headers.get('stripe-signature')

  // var to hold event data
  let event

  // verify it
  try {
    event = stripe.webhooks.constructEvent(body, signature, env.STRIPE_WEBHOOK_SECRET)
  } catch (err) {
    // signature is invalid!
    console.warn('⚠️  Webhook signature verification failed.', err.message)

    // return, because it's a bad request
    throw error(400, 'Invalid request')
  }

  // signature has been verified, so we can process events
  // full list of events: https://stripe.com/docs/api/events/list
  if (event.type == 'charge.succeeded') {
    // get data object
    const charge = event.data.object

    // TODO: fulfill the order here
    console.log(`✅ Charge succeeded ${charge.id}`)
  }

  // return a 200 with an empty JSON response
  return json()
}

code

In development mode, webhooks can be routed to your dev machine using Stripe’s CLI. Example:

stripe listen --forward-to localhost:5173/stripe/webhooks

For more information on webhooks, see Stripe’s Webhook Docs.

Styling

Components can be styled by setting attributes on the <Elements/> container.

<Elements
  theme="flat"
  labels="floating"
  variables={{ colorPrimary: 'pink' }}
  rules={...}
  />

See appearance docs for more examples.

Examples

All demos are running in test-mode, any of Stripe’s test card numbers will work.

Sponsors

This project is made possible by:

Stripe's logo