A question was interestingly put on the WP Business Builders Facebook group about how to accept donations with a low-profile form but process it through SureCart. Now, the poster said with Fluent Forms or SureForms, I chose the latter, given the audience within that group.
This article is how I approached this, and I would solve it using a practical, real-world example. The original poster actually posted a reference url
The first thing i did was build a facsimile of the form, not from a stylisation point of view but in terms of functionality, and then I worked out what the minimum amount of data I needed to pass from the form via URL parameters to my next step.
So creating the form in SureForm minus styling and conditional setup was easy, I didn’t need these as this isn’t a design issue it’s a process one

Behind the Form
This bit was easy. Sureform allows for on-submission redirect and URL parameters to be set, so the next thing I needed to work out was what I would need to send. In the end, I settled on a donation type, i.e once or subscription, the monthly, once amounts and a field to allow them to have custom amounts too.

The next decision
As the above image shows, the next decision was how I would process this, as I couldn’t have a single product for both the subscription and the one off purchase so I knew I needed two and i needed this to be dynamic, so the way I deciced to handle this was with a shortcode that would read the url paramaters and from there decide where to reroute it and convert our custom or fixed amount to single unifed donation_amount.
I used a page called donation and added the shortcode, which would be donation_redirect. This would function as the trigger for the code below. I’ve used this before, and most recently as a method for redirecting people back to posts they had purchased using another of my plugins.

The code is customisable to some degree, allowing you to have two places to redirect to based on the donation types, in my case, they are SureCart instant checkout pages.
<?php
/**
* Redirect donation router page based on query params.
* Uses [donation_redirect] as a marker shortcode on the page.
*/
// 1) Register a simple marker shortcode.
add_action( 'init', function () {
add_shortcode( 'donation_redirect', '__return_empty_string' );
} );
// 2) On template_redirect, if page has [donation_redirect], build target URL & redirect.
add_action( 'template_redirect', function () {
// Only on single posts/pages.
if ( ! is_singular() ) {
return;
}
global $post;
if ( ! $post instanceof WP_Post ) {
return;
}
// Only act if the current page actually has the marker shortcode.
if ( ! has_shortcode( $post->post_content, 'donation_redirect' ) ) {
return;
}
// ---- CONFIG: adjust these to your needs ----
$monthly_url = 'https://shrimp-wasp-760.wpta.uk/buy/subscription-donation';
$onetime_url = 'https://shrimp-wasp-760.wpta.uk/buy/one-time-donation/';
$type_param = 'donation_type';
$monthly_value = 'Monthly'; // what your URL actually sends
$onetime_value = 'Once'; // what your URL actually sends
// Input amount params from your form / URL
$monthly_amount_param = 'monthly_amount';
$monthly_custom_param = 'monthly_amount_custom';
$onetime_amount_param = 'once_amount';
$onetime_custom_param = 'once_custom';
// Unified output param
$output_amount_param = 'donation_amount';
// ---- END CONFIG ----
// Need donation_type in the query.
if ( ! isset( $_GET[ $type_param ] ) ) {
return;
}
$donation_type_raw = sanitize_text_field( wp_unslash( $_GET[ $type_param ] ) );
$donation_type = strtolower( $donation_type_raw );
$monthly_value_lc = strtolower( $monthly_value );
$onetime_value_lc = strtolower( $onetime_value );
$is_monthly = false;
$target_url = '';
if ( $donation_type === $monthly_value_lc ) {
$is_monthly = true;
$target_url = $monthly_url;
} elseif ( $donation_type === $onetime_value_lc ) {
$is_monthly = false;
$target_url = $onetime_url;
} else {
// Unknown donation_type -> do nothing
return;
}
// Build params array from current query string.
$params = array();
if ( ! empty( $_GET ) && is_array( $_GET ) ) {
foreach ( $_GET as $key => $value ) {
$key_sanitized = sanitize_key( $key );
if ( is_array( $value ) ) {
$params[ $key_sanitized ] = array_map( 'sanitize_text_field', wp_unslash( $value ) );
} else {
$params[ $key_sanitized ] = sanitize_text_field( wp_unslash( $value ) );
}
}
}
// Decide amount to use: try to extract a numeric value, with sane fallbacks.
$amount = '';
$fallback_raw = '';
if ( $is_monthly ) {
// For monthly donations, prefer monthly fields, then fall back to once fields.
$candidate_keys = array(
$monthly_custom_param, // custom monthly
$monthly_amount_param, // preset monthly
$onetime_custom_param, // fallback custom once
$onetime_amount_param, // fallback preset once
);
} else {
// For one-time donations, prefer once fields, then fall back to monthly fields.
$candidate_keys = array(
$onetime_custom_param, // custom once
$onetime_amount_param, // preset once
$monthly_custom_param, // fallback custom monthly
$monthly_amount_param, // fallback preset monthly
);
}
foreach ( $candidate_keys as $key ) {
if ( empty( $params[ $key ] ) ) {
continue;
}
$candidate = trim( $params[ $key ] );
// Remember the first non-empty value as a last-resort fallback
if ( $fallback_raw === '' ) {
$fallback_raw = $candidate;
}
// Try to extract a number (handles "25", "25.00", "£25", "Other Amount 25", etc.)
if ( preg_match( '/\d+(?:\.\d+)?/', $candidate, $matches ) ) {
$amount = $matches[0];
break;
}
}
// If we found no numeric content at all but had *some* text, use that as a fallback
if ( $amount === '' && $fallback_raw !== '' ) {
$amount = $fallback_raw;
}
// Remove original amount params
unset(
$params[ $monthly_amount_param ],
$params[ $monthly_custom_param ],
$params[ $onetime_amount_param ],
$params[ $onetime_custom_param ]
);
// Set unified donation_amount if we have one
if ( $amount !== '' ) {
$params[ $output_amount_param ] = $amount;
}
// Build final target URL with remaining params
if ( ! empty( $params ) ) {
$query_string = http_build_query( $params );
$separator = ( strpos( $target_url, '?' ) !== false ) ? '&' : '?';
$target_url = $target_url . $separator . $query_string;
}
// Avoid redirect loop (checkout URL is different, but be safe)
$current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
if ( rtrim( $current_url, '/' ) === rtrim( $target_url, '/' ) ) {
return;
}
wp_safe_redirect( $target_url );
exit;
} );PHPUpdate SureCart to accept our donation amount
This was the only part of the puzzle I knew I could do, but would require some manipulation of the shadow dom of the checkout. Having danced this previously with url-paramaters-for-surecart, I knew this was possible, and I just needed to work on an exact workflow.
Firstly, the product types had to be name your price so we could have a customised amount. This was easily done and set with a minimum/default of zero, so they look presentable when you first arrive.

So after wrestling with the shadow dom for a while, working my way through the JavaScript ,I settled on the following snippet, which again could be adjusted.
<?php
add_action('wp_enqueue_scripts', function () {
wp_register_script('rup-donation-amount-deep', '');
wp_enqueue_script('rup-donation-amount-deep');
$js = <<<'JS'
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
let raw = params.get('donation_amount');
if (!raw) return;
raw = String(raw).trim();
const m = raw.match(/\d+(?:\.\d+)?/);
if (!m) {
console.warn('[donation_amount] No numeric amount in', raw);
return;
}
const amount = parseFloat(m[0]);
if (isNaN(amount)) {
console.warn('[donation_amount] NaN from', m[0]);
return;
}
const fixed = amount.toFixed(2);
console.log('[donation_amount] Parsed amount:', fixed);
// ----------------------------------------------------------------
// Deep search helpers (walk DOM + all shadow roots)
// ----------------------------------------------------------------
function findDeep(predicate, root = document) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
if (!node) continue;
if (node.shadowRoot) {
stack.push(node.shadowRoot);
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const el = node.children[i];
if (predicate(el)) return el;
stack.push(el);
}
}
}
return null;
}
function waitFor(getter, cb, tries = 50, delay = 150, name = 'unknown') {
(function attempt() {
const el = getter();
if (el) {
cb(el);
return;
}
if (tries-- <= 0) {
console.warn('[donation_amount] Timeout waiting for', name);
return;
}
setTimeout(attempt, delay);
})();
}
// ----------------------------------------------------------------
// ELEMENT GETTERS
// ----------------------------------------------------------------
// Outer <sc-button class="selected-price__change-amount">
function getChangeAmountScButton() {
return findDeep(el =>
el.tagName === 'SC-BUTTON' &&
el.classList.contains('selected-price__change-amount')
);
}
// Real <button> inside Change Amount's shadow root
function getChangeAmountButton() {
const scBtn = getChangeAmountScButton();
if (!scBtn || !scBtn.shadowRoot) return null;
return scBtn.shadowRoot.querySelector('button');
}
// The real price input: <input class="input__control" inputmode="decimal">
function getPriceInput() {
return findDeep(el =>
el.tagName === 'INPUT' &&
el.classList.contains('input__control') &&
el.getAttribute('inputmode') === 'decimal'
);
}
// The inline Update sc-button inside sc-price-input (slot="suffix" submit)
function getUpdateScButton() {
return findDeep(el =>
el.tagName === 'SC-BUTTON' &&
el.hasAttribute('submit') &&
el.getAttribute('slot') === 'suffix' &&
!!el.closest('sc-price-input')
);
}
function getUpdateButton() {
const scBtn = getUpdateScButton();
if (!scBtn || !scBtn.shadowRoot) return null;
return scBtn.shadowRoot.querySelector('button');
}
// ----------------------------------------------------------------
// CLEANUP HELPERS: remove update + change buttons (and keep them gone)
// ----------------------------------------------------------------
function removeInlineButtonsOnce() {
const updateScBtn = getUpdateScButton();
if (updateScBtn) {
updateScBtn.remove();
console.log('[donation_amount] Update button removed');
}
const changeScBtn = getChangeAmountScButton();
if (changeScBtn) {
changeScBtn.remove();
console.log('[donation_amount] Change Amount button removed');
}
}
function startButtonCleanupObserver() {
// Run once immediately
removeInlineButtonsOnce();
// Then watch for future DOM changes and remove again if they reappear
const observer = new MutationObserver(() => {
removeInlineButtonsOnce();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// ----------------------------------------------------------------
// 1) Click Change Amount
// ----------------------------------------------------------------
waitFor(
getChangeAmountButton,
function (btn) {
console.log('[donation_amount] Change Amount button found, clicking…');
btn.click();
// ----------------------------------------------------------------
// 2) Wait for input, set value
// ----------------------------------------------------------------
waitFor(
getPriceInput,
function (input) {
console.log('[donation_amount] Input found, setting value:', fixed);
input.focus();
input.value = fixed;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
// ----------------------------------------------------------------
// 3) Wait for Update button, click it
// ----------------------------------------------------------------
waitFor(
getUpdateButton,
function (ub) {
console.log('[donation_amount] Update button found, clicking…');
ub.click();
// After click, start persistent cleanup of both buttons
setTimeout(startButtonCleanupObserver, 300);
},
50,
150,
'Update button'
);
},
50,
150,
'price input'
);
},
50,
150,
'Change Amount button'
);
});
JS;
wp_add_inline_script('rup-donation-amount-deep', $js);
});
PHPThe Final Solution
The solution, when put all together, works and is reasonably slick, it won’t alarm people, and most users wouldn’t even see what is going on It’s an easy way of using SureCart to checkout items you could even couple it with custom fields output and have it store the custom data for you i.e sizing and other customsisation that would be easier in a form than a checkout.
You can see in the gif below the final version working all together

