Late last year after some requests I opened a PayPal.me page for people to donate/tip me money for my projects. I’d never set one up before because I never thought highly enough of my own work to think someone would want to tip me for it, even though it is free. However, people did. Enough people that I then started getting requests to move off PayPal for various reasons (related to fees and issues people were having.) So being me, I decided to build my own.
The obvious choice to collect payments online is Stripe. Stripe is a phenomenal payment processor, and they also support Apple Pay (and the other Payment Request APIs from non-Apple browsers), so I decided to see how quickly I could whip up a small tipping page.
From my marketing degree, and time selling software on the App Store (including having tip jars in apps), I know that I needed to have some predefined tipping amounts, with one already selected. This would be the easiest way for people to not have to worry if they’re not tipping the right amount. I settled on $5, $10, and $20, with an other field. I wanted the page to be super simple so I styled the radio choices as big buttons to choose from. I also styled the “other” text input the same way. You can see the css and html here, it covers how I make the radio buttons and text input all look like the same button.
Then I had to integrate Stripe. Out of the box, Stripe is super easy. I followed their tutorial almost exactly (using a custom button). I then also added a PayPal button that linked to my page with the amount pre-filled.
All the of the radio buttons had the on change handler onclick="amountChanged();"
and the text input has oninput="handleOtherChange();"
. These are:
function amountChanged() {
document.getElementById('otheramount').value = ""
document.getElementById('otheramount').className = "";
updateRequest()
}
function handleOtherChange() {
let otherField = document.getElementById('otheramount');
if (otherField.value != "") {
otherField.className = "active";
uncheckAmountOptions()
updateRequest()
} else {
otherField.className = "";
}
}
Amount Changed sets the other field to blank, and updates Stripe. Handle Other Change checks there is something in the field, and sets the text input to active and removes the checked radio button. Handled by this lazy, but effective, function:
function uncheckAmountOptions() {
document.getElementById("500").checked = false;
document.getElementById("1000").checked = false;
document.getElementById("2000").checked = false;
}
Finally I update the payment request so that the Apple Pay button has the correct amount set for it:
function updateRequest() {
let amount = Number(getAmount())
document.getElementById('amountoutput').innerText = amount / 100
paymentRequest.update({
currency: 'usd',
total: {
label: 'Tip Pat Murray',
amount: amount,
}
})
}
Get Amount just queries which amount should be charged:
function getAmount() {
let otherField = document.getElementById('otheramount');
if (otherField.value != "") {
return otherField.value * 100
} else {
let radios = document.getElementsByClassName('radio-button');
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
return radios[i].value
}
}
}
}
The get amount function is also called for generating the PayPal URL and the Stipe credit card button:
document.getElementById('stripebutton').addEventListener('click', function (e) {
// Open Checkout with further options:
handler.open({
name: "Pat Murray's Tip Jar",
currency: 'usd',
amount: Number(getAmount()),
allowRememberMe: false
});
e.preventDefault();
});
Lastly I process the payment using an Azure Function that just takes the token and the amount and generates a charge. I don’t even log anything in Azure as I can just see the charges in Stripe/PayPal respectively. The Function also calls Twilio to sms me when I receive a tip as it is a massive morale boost to know someone thinks so highly of my stuff.
An abridged version of the function code is below
const stripe = require("stripe")("XXX");
module.exports = function (context, req) {
if (req.body && req.body.token && req.body.amount) {
let token = req.body.token;
let amount = req.body.amount;
// Create a charge in the stripe API
let charge = stripe.charges.create({
amount: amount,
currency: 'usd',
description: "Pat Murray's Tip Jar",
source: token,
metadata: {
message: req.body.message
}
}).then((charge) => {
// New charge created. record charge object
context.log(charge);
if (charge.paid) {
context.res = {
status: 200, /* Defaults to 200 */
body: charge.paid
};
// Sned myself an SMS with the name and contents of the charge
let msg = "🎉 " + charge.source.name + " tipped $"+ charge.amount/100 + "!\n " + charge.metadata.message + ""
context.bindings.message = {
body : msg,
};
} else {
context.res = {
status: 400, /* Defaults to 200 */
body: charge.paid
};
}
context.done();
}).catch((err) => {
// charge failed. Alert user that charge failed somehow
switch (err.type) {
......
}
context.res = {
status: 400, /* Defaults to 200 */
body: false
};
});
} else {
context.res = {
status: 400
};
context.done();
}
};
If you want to see the page or even play around with Apple Pay on the web by… idk… giving me some money, visit patmurray.co/tip