svelte-stripe

Everything you need to accept Stripe payments with your Svelte projects. SvelteKit is fully supported.

Links: npm github changelog license

Installation

To configure your project add these 3 packages:

pnpm install -D svelte-stripe @stripe/stripe-js stripe

Docs

Set up Stripe

Add your private and public keys to your environment:

VITE_STRIPE_PUBLIC_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

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

<script>
  import { loadStripe } from '@stripe/stripe-js'
  import { Container } from  'svelte-stripe'

  let stripe = null

  onMount(async () => {
    stripe = await loadStripe("pk_test_51L0t1EHfKEQGgXe5hXbsYpwnEJ4stYZRN6r8gyd6eftJTdLD575EEKFE1uLF0suchc1J26nCU6xyQtepi1dvtc3Q00qsgQsZbN")
  })
<script>

{#if stripe}
  <Container {stripe}>
    <!-- this is where your Stripe components go -->
  </Container>
{/if}

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.js to create the “payment intent”:

import Stripe from 'stripe'

// initialize Stripe
const stripe = new Stripe(process.env['STRIPE_SECRET_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}>
  <PaymentElement {stripe} {clientSecret} bind: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

Credit Cards

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

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

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

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/>.

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

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:

<Container {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>
</Container>

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:

<Container {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>
</Container>

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.js
import Stripe from 'stripe'

// init api client
const stripe = new Stripe(process.env['STRIPE_SECRET_KEY'])

// get webhook secret
// find yours at https://dashboard.stripe.com/webhooks
const endpointSecret = process.env['STRIPE_WEBHOOK_SECRET']

// endpoint to handle incoming webhooks
export async function post(request) {
  // convert raw body to buffer
  const rawBody = Buffer.from(request.rawBody)

  // get the signature from the header
  const signature = request.headers['stripe-signature']

  // var to hold event data
  let event

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

    // return, because it's a bad request
    return { status: 400 }
  }

  // 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 status 200
  return {}
}

code

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

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

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

Styling

TODO

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