Allow custom/required fields (e.g. consent checkbox) on the Setup Wizard payment step for logged-in users

Category: Setup Wizard / Custom Fields

Description:

Currently, custom fields can only be attached to the Setup Wizard via a Registration form — but that form (and any fields on it) is automatically skipped when the user is already logged in. This means there is no way to require a field at the point of payment/plan-selection for existing members who are renewing, upgrading, or purchasing an additional plan.

This is a problem for any site that needs to capture a legally required affirmative action at checkout rather than at signup. In our case, EU Directive 2011/83/EU Art. 16(m) requires an explicit consent action from the customer to waive their 14-day withdrawal right on digital content/services — and that consent has to be captured at the moment of purchase, not buried in a policy page or only shown to new registrants.

Requested feature:
Add the ability to attach custom fields (or at minimum a required checkbox/agreement field type) directly to the plan/payment step of the Setup Wizard, independent of the Registration form — so it displays and is enforced for logged-in members purchasing/renewing/changing a plan, not just at initial signup.

Why it matters:
Any ARMember site with legal, contractual, or compliance requirements tied to the purchase moment (consent, waivers, terms acceptance, age confirmation, etc.) needs this, and right now the only path is building custom validation against internal hooks that aren't documented extension points — which is fragile across plugin updates.

We currently have a working mu-plugin implementation using arm_after_setup_gateway_section, arm_validate_field_value_before_form_submission, and arm_after_setup_form_validate_action, and are happy to share the exact code with your dev team if it helps evaluate this.

Current implementation workaround via mu-plugins (fix-arm-refund-waiver-consent.php):

<?php

/* Adds a required "immediate access / refund waiver" checkbox to ARMember's

   plan checkout (the [arm_setup] form) and blocks checkout server-side

   unless it's checked — see refunds.html Section 3, which states the

   14-day EU/UK withdrawal right is waived once a subscribed module is

   accessed. ARMember's own custom-field system can't cover this: its

   registration/custom fields are skipped entirely for logged-in users

   (confirmed in class.arm_membership_setup.php, arm_member_validate_meta_details()

   call with $is_validate_form_field = 0), so a native field would only ever

   apply to brand-new signups, not existing members buying/renewing a plan.

   Hooking ARMember's own render + validation pipeline instead, without

   touching its core files (would be wiped on plugin update):

   - arm_after_setup_gateway_section: filters the checkout form's HTML while

     it's still being assembled, before </form> is written, so the injected

     checkbox is a real field that gets POSTed. Fires for both new-signup and

     logged-in-member checkout, since only the *registration* module (not the

     payment/gateway module) is skipped when logged in.

   - arm_validate_field_value_before_form_submission: fires unconditionally

     at the end of ARMember's arm_member_validate_meta_details(), regardless

     of $is_validate_form_field — the one validation point common to both

     paths. Returning an errors array here blocks checkout the same way

     ARMember's own required-field errors do.

   - arm_after_setup_form_validate_action: fires only once validation has

     already passed, so by the time this runs we know the box was checked —

     used here purely to write an audit-trail record (timestamp, IP, user

     agent) against the user, as evidence if a chargeback or withdrawal

     claim is ever disputed. */

define('QBW_REFUND_WAIVER_FIELD_SLUG', 'qbw_refund_waiver_consent');

function qbw_refund_waiver_is_paid_checkout($post_data) {

    return !empty($post_data['setup_action']) && $post_data['setup_action'] === 'membership_setup'

        && (!empty($post_data['payment_gateway']) || !empty($post_data['_payment_gateway']));

}

function qbw_refund_waiver_label_text() {

    $lang = function_exists('pll_current_language') ? pll_current_language() : substr(get_locale(), 0, 2);

    $labels = [

        'it' => 'Richiedo l’accesso immediato al Servizio e riconosco di perdere il mio diritto di recesso di 14 giorni non appena accedo a un modulo sottoscritto. Consulta la nostra Politica di Rimborso e Cancellazione.',

        'en' => 'I request immediate access to the Service and acknowledge that I lose my 14-day right of withdrawal once I access any subscribed module. See our Refund & Cancellation Policy.',

    ];

    return $labels[$lang] ?? $labels['en'];

}

/* Render: append the checkbox to the payment/gateway section of the checkout form. */

add_filter('arm_after_setup_gateway_section', function ($module_content) {

    $text = esc_html(qbw_refund_waiver_label_text());

    $module_content .= '<div class="arm-df__form-field arm_module_box" style="margin:16px 0;padding:14px 16px;border:1px solid var(--color-divider, #ccc);">'

        . '<label style="display:flex;align-items:flex-start;gap:10px;font-size:.92rem;line-height:1.5;cursor:pointer;">'

        . '<input type="checkbox" name="' . esc_attr(QBW_REFUND_WAIVER_FIELD_SLUG) . '" value="1" required style="margin-top:3px;flex-shrink:0;">'

        . '<span>' . $text . '</span>'

        . '</label></div>';

    return $module_content;

}, 10, 1);

/* Validate: block checkout server-side if the box wasn't checked. */

add_filter('arm_validate_field_value_before_form_submission', function ($return, $armform, $posted_data) {

    if (!qbw_refund_waiver_is_paid_checkout($posted_data) || !empty($posted_data[QBW_REFUND_WAIVER_FIELD_SLUG])) {

        return $return;

    }

    $errors = is_array($return) ? $return : [];

    $errors[QBW_REFUND_WAIVER_FIELD_SLUG] = qbw_refund_waiver_label_text();

    return $errors;

}, 10, 3);

/* Audit trail: log consent once checkout has actually passed validation. */

function qbw_record_refund_waiver_consent($user_id, $context) {

    if (empty($user_id)) {

        return;

    }

    $log = get_user_meta($user_id, 'qbw_refund_waiver_consent_log', true);

    $log = is_array($log) ? $log : [];

    $log[] = [

        'time'    => current_time('mysql'),

        'ip'      => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '',

        'ua'      => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '',

        'context' => $context,

    ];

    update_user_meta($user_id, 'qbw_refund_waiver_consent_log', $log);

}

/* Existing member buying, changing, or renewing a plan through the setup form. */

add_action('arm_after_setup_form_validate_action', function ($setup_id, $post_data) {

    if (empty($post_data[QBW_REFUND_WAIVER_FIELD_SLUG]) || !is_user_logged_in()) {

        return;

    }

    qbw_record_refund_waiver_consent(get_current_user_id(), 'setup:' . $setup_id);

}, 10, 2);

/* Brand-new signup (including signup-with-plan-purchase through the setup wizard,

   which creates the account via the same arm_register_new_member() call). */

add_action('arm_after_add_new_user', function ($user_id, $posted_data) {

    if (empty($posted_data[QBW_REFUND_WAIVER_FIELD_SLUG])) {

        return;

    }

    qbw_record_refund_waiver_consent($user_id, 'registration');

}, 10, 2);