Skip to main content

Configurations

The following settings are relevant for configuring the Stripe payment method:

Modules

The following modules are relevant for integrating the Stripe payment method in the checkout process:
  • $wsStripe - Stripe payment methods module
  • $wsCheckout - Checkout state, addresses, shipping, payment, problems, totals
  • $wsActions - Generate and evaluate actions
  • $wsAccount - Login status, email, addresses, loadAddress()
  • $wsViews - Current URL, destination pages, view URLs
  • $wsBasket - Basket and order overview
  • $wsConfig - Configuration values, for example salutations and currency

Actions

The following actions are relevant for the integration of the Stripe payment method:

Additionally relevant information

The following additional content must be integrated for the payment method.

Frontend integration

Stripe provides its own JavaScript SDK that must be embedded in the template. It handles the display of the payment fields and the communication with Stripe. HTML structure
The following HTML block must be placed in the template:
  • #wsStripePaymentElement is the container in which Stripe automatically embeds the payment fields (e.g., credit card number, PayPal button, etc.)
  • The form submits the payment process when “Pay with Stripe” is clicked
<div id="wsStripePaymentElement">
    
</div>
<form method="post" action="{{= $wsViews.current.url() }}" id="wsCheckoutStripeConfirmForm">
    <input type="hidden" name="wsact" value="{{= $cActionCheckoutConfirm.id }}">
    <input type="hidden" name="wscsrf" value="{{= $cActionCheckoutConfirm.csrf }}">
    <input type="hidden" name="wstarget" value="{{= $wsViews.viewUrl('confirm.htm') }}">
    <button class="btn btn-success btn-block wsStripeOrderBtn" {{if not $wsCheckout.isValid}}disabled{{/if}}>
      Pay with Stripe
    </button>
</form>
JavaScript
The following script block must be inserted directly after the HTML block. It must be enclosed in {{ autoescape "js" }} so that template variables in the JavaScript context are processed correctly.
The flow in the script is as follows:
  1. Initialize Stripe - The SDK is started with the credentials from $wsStripe.configuration
  2. Display payment fields - Stripe renders the payment selection (credit card, PayPal, etc.) into the #wsStripePaymentElement container. The Stripe address input fields are disabled because the customer’s address is already known in the shop and is passed automatically.
  3. Trigger payment - When the form is submitted, the entered payment data is sent to Stripe together with the billing address from $wsAccount. Stripe returns a confirmation token for this.
  4. Send token to the shop - The token is transmitted to the shop, which uses it to start the actual payment process with Stripe.
  5. Process result - If the payment fails, an error message is displayed. If an additional confirmation is required (e.g., 3D Secure), Stripe handles this automatically.
  6. Redirect - After a successful payment, the customer is redirected to the order confirmation page.
{{ autoescape "js" }}
<script>
    {{ var $stripeConfig = $wsStripe.configuration }}
    const stripe = Stripe("{{= $stripeConfig.publishableKey }}", {
        stripeAccount: "{{= $stripeConfig.targetAccount }}"
    });

    const appearance = {
        theme: 'stripe', // Appearance customizable, see Stripe Appearance API
    };

    const options = {
        mode: 'payment',
        amount: {{= $wsCheckout.getAmountInSmallestUnit($wsCheckout.sum.total) }},
        currency: '{{= lower($wsCheckout.sum.currency) }}',
        paymentMethodCreation: 'manual',
        appearance,
    };

    const elements = stripe.elements(options);

    // Disable Stripe address input fields – the address is
    // passed automatically below from $wsAccount when submitting
    const paymentElement = elements.create('payment', {
        fields: {
            billingDetails: {
                name: 'never',
                email: 'never',
                phone: 'never',
                address: {
                    line1: 'never',
                    line2: 'never',
                    city: 'never',
                    state: 'auto', // State is not present in our addresses
                    country: 'never',
                    postalCode: 'never'
                }
            }
        }
    });
    paymentElement.mount("#wsStripePaymentElement");

    document
        .querySelector("#wsCheckoutStripeConfirmForm")
        .addEventListener("submit", handleSubmit);

    async function handleSubmit(e) {
        e.preventDefault();
        setLoading(true);

        // Check whether the payment inputs are complete
        const {error: submitError} = await elements.submit();
        if (submitError) {
            showMessage(submitError.message);
            return;
        }

        // Send payment data + billing address to Stripe,
        // Stripe returns a confirmation token for this
        const {error, confirmationToken} = await stripe.createConfirmationToken({
            elements,
            params: {
                payment_method_data: {
                    billing_details: {
                        {{ foreach $addr in $wsAccount.addresses }}
                            {{if $addr.id==$wsCheckout.selectedBillAddress}}
                        address: {
                            city: "{{=$addr.city}}",
                            country: "{{=$addr.country}}",
                            line1: "{{=$addr.street}}",
                            line2: "",
                            postal_code: "{{=$addr.zip}}",
                        },
                        email: "{{ if $wsCheckout.guestMail }}{{= $wsCheckout.guestMail }}{{ else }}{{= $wsAccount.email }}{{ /if }}",
                        name: "{{=$addr.firstName}} {{=$addr.lastName}}",
                        phone: "{{= $addr.phone }}",
                            {{ /if }}
                        {{ /foreach }}
                    }
                }
            }
        });

        if (error) {
            showMessage(error.message);
            return;
        }

        // Send the confirmation token to the shop, which uses it to start the payment process
        var action = $('#confirmForm').attr('action');
        var payload = $('#confirmForm').serialize();
        payload += `&confirmationToken=${confirmationToken.id}`;

        const response = await fetch(action, {
            method: 'post',
            body: payload
        });

        const dataContentType = response.headers.get("content-type");
        if (dataContentType && !dataContentType.includes("application/json")) {
            // Response is not JSON (e.g., if the terms and conditions were not accepted) – reload the page
            return;
        }

        const data = await response.json();
        const { success } = data;

        if (success === false) {
            // Payment declined – error details at: https://docs.stripe.com/testing#declined-payments
            const {error_code, decline_code} = data;
            showMessage(`Payment failed: ${error_code} ${decline_code}`);
            setLoading(false);
            return;
        }

        // If an additional confirmation is required (e.g., 3D Secure),
        // Stripe handles this automatically
        const { clientSecret, status } = data;
        if (status === "requires_action") {
            const { error } = await stripe.handleNextAction({ clientSecret });
            if (error) {
                showMessage(error.message);
                setLoading(false);
                return;
            }
        }

        setLoading(false);
        // Payment successful – redirect to the order confirmation
        window.location.href = "{{= $wsViews.viewUrl('confirm.htm', {wsPayment: 'stripe', stripe_action: 'return'}) }}";
    }

    // ------- UI helper functions -------

    function showMessage(messageText) {
        // TODO: display error message
    }

    function setLoading(isLoading) {
        let wsStripeOrderBtn = document.querySelectorAll(".wsStripeOrderBtn");
        wsStripeOrderBtn.forEach(btn => btn.disabled = isLoading);
        // TODO: show/hide loading indicator
    }
</script>
{{ /autoescape }}