Stripe.me and PayPal.me donation form

Posted in Posted on 2018-12-29 20:55

I have built a web form which lets people give arbitrary amounts of money via either Stripe (a payment processor that takes credit cards) or PayPal. The form is easy to use and provides for multiple currencies. The best thing is that you don’t need any server-side code to make it work and you don’t need to have another company host it and take a cut.

Introduction

Sometimes kind people donate money to me for the Morse code tools on my website via a “PayPal.me” link which is a PayPal-hosted web page where you can enter the amount you want to give. I think that some time ago PayPal let people donate money via a credit card without having a PayPal account but now it seems (that unless you are a charity) you need an account, so that’s a problem for some.

Someone recently asked me if they could donate some money using a credit card so I set about creating a donation page to allow this. Stripe is a large well-known credit card payment processor with a modern administrative interface and a good API but there’s no such thing as a “Stripe.me” page. I’ve made something as simple as I could to go with the “PayPal.me” link so that people can give money via Stripe or PayPal as they like.

Stripe takes a cut of any funds passing through its site but it works well and the fees are not unreasonable (in the UK it is 1.4% + 20p for European cards or 2.9% + 20p for other cards). I thought that Stripe might have a “Stripe.me” type page where people could go to put money into my Stripe account. For some reason they do not so I Googled for solutions that other people had already built.

There are a lot of web services providing donation forms, aimed particularly at charities but they (almost certainly) all take another cut of any donation amount. There are also instructions around for how to integrate with Stripe, both Stripe’s own excellent documentation and libraries and other bloggers, but they all involve server-side code. Now, I am more than capable of dealing with the server-side code but I was annoyed that it was necessary: it would just be one more thing to maintain.

Stripe.me

Although Stripe does not have a way to donate an arbitrary amount, it does have a system called “Checkout” which lets you create “products” in the adminitrative dashboard and assign “SKUs” (stock keeping units) to the products with different monetary amounts associated with each SKU (these are intended to represent different variations of a product such as different sizes). Arbitrary numbers of an SKU can be purchased through a Stripe-hosted web page. As it says in the docs: “You can use Checkout without using the Stripe API on your server. This makes it ideal for conventional websites or standalone front-end applications. It’s the fastest way to start accepting payments with Stripe.”

This led me to the following cunning plan:

  • Create a product called “SCPhillips.com donation”;
  • add a 1 unit SKU for each currency I want to support e.g. with $1, £1, €1;
  • create a form that purchases an arbitrary number of these 1 unit SKUs;
  • adapt the Javascript supplied by Stripe to work with the above hack.

Creating the SKUs

First you need to set up a Stripe account of course, and then add a product and as many SKUs as currencies that you want to natively support (if someone has a card in another currency they can still make a donation but there will not be any suggested amounts in their own currency so it will be harder for them to judge how much they are giving). My dashboard ended up looking like so:

The product and SKUs in the Stripe dashboard

Stripe’s code

As described in the Stripe docs once you have created the product and an SKU you can click on the “Use with checkout” button to generate a Javascript snippet to go in your webpage. For one of my SKUs the generated HTML and Javascript snippet was as follows:

<!-- Load Stripe.js on your website. -->
<script src="https://js.stripe.com/v3"></script>

<!-- Create a button that your customers click to complete their purchase. -->
<button id="checkout-button">Pay</button>
<div id="error-message"></div>
var stripe = Stripe('pk_live_6rgvVA9EKhrMEEKCSlQzhQ4e', {
    betas: ['checkout_beta_4']
});

var checkoutButton = document.getElementById('checkout-button');
checkoutButton.addEventListener('click', function () {
    // When the customer clicks on the button, redirect
    // them to Checkout.
    stripe.redirectToCheckout({
        items: [{sku: 'sku_EBBTCgLzZallj0', quantity: 1}],

        // Note that it is not guaranteed your customers will be redirected to this
        // URL *100%* of the time, it's possible that they could e.g. close the
        // tab between form submission and the redirect.
        successUrl: 'https://scphillips.com/success',
        cancelUrl: 'https://scphillips.com/canceled',
    })
    .then(function (result) {
        if (result.error) {
        // If `redirectToCheckout` fails due to a browser or network
        // error, display the localized error message to your customer.
        var displayError = document.getElementById('error-message');
        displayError.textContent = result.error.message;
        }
    });
});

This is straightforwward enough: a button is created and a “click” event handler is attached to it which calls the Stripe checkout system passing in the chosen SKU and a “quantity” parameter of “1”. Now it needs adapting to take different payment amounts.

The design

I looked around at various donation pages, such as Mozilla and Wikimedia and (a) decided I liked the Mozilla one and (b) hoped they had done some UX work to come up with a good design. I was therefore aiming at something like this:

Mozilla donation page

The HTML form

Here is a stripped-down version of my HTML form. It contains the bare essentials but will not look particularly fancy. If you want to see the styled version take a look and right-click to view the source. My website uses Foundation to make rows and columns but as there are so many alternative frameworks it’s not worth cluttering the HTML here with all of that.

<!-- Load Stripe.js on your website. -->
<script src="https://js.stripe.com/v3"></script>

<h3>Choose amount</h3>

<select id="currency">
    <option value="USD" selected>USD $</option>
    <option value="GBP">GBP £</option>
    <option value="EUR">EUR €</option>
</select>
<input type="radio" class="amount" name="amount" value="0" id="amount-0">
<label for="amount-0" class="amount"></label>
<input type="radio" class="amount" name="amount" value="1" id="amount-1">
<label for="amount-1" class="amount"></label>
<input type="radio" class="amount" name="amount" value="2" id="amount-2">
<label for="amount-2" class="amount"></label>
<input type="radio" class="amount" name="amount" value="3" id="amount-3">
<label for="amount-3" class="amount"></label>
<input type="radio" class="amount" name="amount" value="4" id="amount-4">
<label for="amount-4" class="amount"></label>
<input id="number" type="number" pattern="\d+" title="Please enter a whole number" placeholder="Other amount"/>
<div id="error-message"></div>

<h3>Choose payment method</h3>

<button id="checkout-stripe">Credit/Debit Card</button>
<a class="button" id="checkout-paypal" href="https://www.paypal.me/StephenCPhillips/">Pay with PayPal</a>

The HTML has five radio buttons, all initially with no label. The labels of the first four radio buttons are changed by the Javascript to have different pre-set donation amounts in. The fifth one just gets the currency code updated as it is adjacent to the text input field.

There are two payment buttons: the Stripe one and a PayPal.me one.

The Javascript

The sample Javascript has to be adapted to (a) change the amounts in the buttons when the chosen currency is changed and (b) pick up the right SKU and the right quantity from the form when the donate button is pressed. There’s also the PayPal button to deal with in this version.

// Publishable Stripe key
var stripe = Stripe('pk_live_6rgvVA9EKhrMEEKCSlQzhQ4e', {
    betas: ['checkout_beta_4']
});

// SKU for each currency
var skus = {
    EUR: 'sku_EECTkbT4J0SGFN',
    USD: 'sku_EBB9MC7svkDbLK',
    GBP: 'sku_EBBTCgLzZallj0'
};

// Preset suggested donation amounts for each currency
var choices = {
    EUR: [3,6,12,25],
    USD: [3,6,12,25],
    GBP: [3,5,10,20]
};

// Get the selected curreny (including currency symbol)
function getCurrency() {
    var selector = document.getElementById('currency');
    var currency = selector.options[selector.options.selectedIndex].innerHTML;
    return currency;
}

// Update donation amounts in buttons
function updateDonationButtons() {
    var currency = getCurrency();
    var currency_code = currency.substr(0,3);
    document.querySelector('#amount-0+label').innerHTML = currency + choices[currency_code][0];
    document.querySelector('#amount-1+label').innerHTML = currency + choices[currency_code][1];
    document.querySelector('#amount-2+label').innerHTML = currency + choices[currency_code][2];
    document.querySelector('#amount-3+label').innerHTML = currency + choices[currency_code][3];
    document.querySelector('#amount-4+label').innerHTML = currency;
}

// Change buttons when currency is changed
var currencySelector = document.getElementById('currency');
currencySelector.addEventListener('change', updateDonationButtons);

// Initialise values in buttons
updateDonationButtons();

// Select final radio button if input box clicked or typed in
document.getElementById('number').addEventListener('click', function(event) {
    document.getElementById('amount-4').checked = true;
});
document.getElementById('number').addEventListener('input', function(event) {
    document.getElementById('amount-4').checked = true;
});

// Put focus in input box if corresponding radio button is selected
document.getElementById('amount-4').addEventListener('click', function(event) {
    document.getElementById('number').focus();
});

// Get the numerical value of the donation amount
function getAmount(currency) {
    var radio = document.querySelector("input[type='radio'][name='amount']:checked");
    if (radio === null) {
        // nothing selected
        return 0;
    }
    if (radio.id === 'amount-4') {
        // "other amount" selected
        amount = document.getElementById('number').value;
        amount = Math.round(parseInt(amount));
        if (isNaN(amount)) {
            amount = 0;
        }
    } else {
        // preset button selected
        amount = choices[currency][radio.value];
    }
    return amount;
}

// Stripe submission
document.getElementById('checkout-stripe').addEventListener('click', function() {

    var displayError = document.getElementById('error-message');

    var currency = getCurrency().substr(0,3);
    var sku = skus[currency];

    var amount = getAmount(currency);
    if (amount === 0) {
        displayError.textContent = "Please choose a valid amount.";
        return;
    }

    stripe.redirectToCheckout({
        items: [{
            sku: sku,
            quantity: amount
        }],
        successUrl: 'https://scphillips.com/donate-success.html',
        cancelUrl: 'https://scphillips.com/donate-canceled.html',
    })
    .then(function(result) {
        if (result.error) {
            // If `redirectToCheckout` fails due to a browser or network
            // error, display the localized error message to your customer.
            displayError.textContent = result.error.message;
        }
    });
});

// PayPal submission
document.getElementById('checkout-paypal').addEventListener('click', function() {
    var currency = getCurrency().substr(0,3);
    var amount = getAmount(currency);
    if (amount > 0) {
        // If an amount has been chosen then update the URL to include it
        this.setAttribute('href', this.getAttribute('href') + amount + currency);
    }
});

I expect most of the above is self-explanatory. When the Stipe button is pressed the code now finds the selected currency and from that selects the right SKU. It then gets the amount that has been chosen and will display an error if nothing has been selected or there is some error in the text field. Stripe is then called as before.

The neat addition is the PayPal button with the code at line 110: if a currency and valid amount has been chosen then they are appended to the paypal.me link to pre-fill the PayPal form. If there is some error in the form then we just take the user to the default PayPal page to complete the amount there.

I hope that helps someone with the same problem. Feel free to copy the Javascript and the form. If you want to leave my Stripe key in there then that would be great, or of course you can just make a donation (but you really don’t have to!). The final page looks like this:

The product and SKUs in the Stripe dashboard

Comments

Comments powered by Disqus