DEV3 Technologies home

Any-amount donation form

A donation form with preset amounts, a custom amount field and strict server-side parsing into minor currency units.

WordPressPaymentsdonationformsanitisingshortcode

Live preview

Code

<?php
/**
 * Donation amount form + amount parser.
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

/**
 * Turn user input like "25", "25.5" or "1,250.00" into minor units (cents).
 * Returns 0 when the value is outside the allowed range.
 */
function acme_parse_donation( string $raw, int $min = 100, int $max = 500000 ): int {
	$clean = preg_replace( '/[^0-9.]/', '', str_replace( ',', '', $raw ) );
	if ( '' === $clean || ! is_numeric( $clean ) ) {
		return 0;
	}
	$minor = (int) round( (float) $clean * 100 );
	return ( $minor >= $min && $minor <= $max ) ? $minor : 0;
}

function acme_donation_form(): string {
	$presets = array( 10, 25, 50, 100 );
	ob_start();
	?>
	<form class="acme-donate" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
		<input type="hidden" name="action" value="acme_donate">
		<?php wp_nonce_field( 'acme_donate', 'acme_nonce' ); ?>
		<fieldset>
			<legend><?php esc_html_e( 'Choose an amount', 'acme-theme' ); ?></legend>
			<?php foreach ( $presets as $amount ) : ?>
				<label class="acme-chip">
					<input type="radio" name="preset" value="<?php echo esc_attr( (string) $amount ); ?>">
					<span><?php echo esc_html( '$' . $amount ); ?></span>
				</label>
			<?php endforeach; ?>
			<label class="acme-custom">
				<?php esc_html_e( 'Other amount', 'acme-theme' ); ?>
				<input type="text" name="custom" inputmode="decimal" autocomplete="off" placeholder="0.00">
			</label>
		</fieldset>
		<button type="submit"><?php esc_html_e( 'Continue', 'acme-theme' ); ?></button>
	</form>
	<?php
	return (string) ob_get_clean();
}
add_shortcode( 'acme_donate', 'acme_donation_form' );

function acme_donate_handle(): void {
	check_admin_referer( 'acme_donate', 'acme_nonce' );

	$raw    = sanitize_text_field( wp_unslash( $_POST['custom'] ?? '' ) );
	$raw    = '' !== $raw ? $raw : sanitize_text_field( wp_unslash( $_POST['preset'] ?? '' ) );
	$amount = acme_parse_donation( $raw );

	if ( 0 === $amount ) {
		wp_safe_redirect( add_query_arg( 'donate_error', '1', wp_get_referer() ?: home_url( '/' ) ) );
		exit;
	}

	// Hand the validated amount to your payment step.
	wp_safe_redirect( add_query_arg( 'amount', $amount, home_url( '/give/confirm/' ) ) );
	exit;
}
add_action( 'admin_post_nopriv_acme_donate', 'acme_donate_handle' );
add_action( 'admin_post_acme_donate', 'acme_donate_handle' );

How it works

What it solves

Charities lose gifts when donors are forced into fixed tiers. An any-amount form lets people pick a preset or type their own figure, while the server converts that figure into an exact, bounded integer.

How it works

Preset amounts are radio buttons styled as chips, with a text field for any other value. A small script makes the two inputs mutually exclusive. On submit the handler verifies the nonce, prefers the custom value, and passes it to acme_parse_donation(), which strips currency symbols and thousands separators, rounds to minor units and enforces a minimum and maximum.

Storing money as integers avoids floating point surprises later.

Adapting it

Change the presets, limits and currency symbol, or read them from options. Replace the final redirect with a call into your payment provider, passing the integer amount. Add a recurring checkbox by validating it against an allow-list. Keep the text input as type="text" with inputmode so locales with commas still work.

Pitfalls

  • Never trust the preset values; they go through the same parser.
  • A 0 result means invalid, so the smallest allowed gift must be above zero.
  • Show a clear message for donate_error and keep the typed value.
  • This is only the amount step; receipts, tax rules and payment security belong to your payment provider.

How to use it

Save as inc/donation-form.php, require it from functions.php, enqueue assets/donation-form.js.

Place [acme_donate] on a giving page.

Create the confirm page and read amount with absint( $_GET['amount'] ?? 0 ).

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets