• Resolved Rune Rasmussen

    (@syntaxerrorno)


    You’re overriding the phone number field, and adds t.ex. +47 automatically into it for Norway.

    This causes a lot of customers to skip the field as it’s already filled, and have no further validation on the phone number length / formatting – so they can checkout without filling it.

    Better remove your override, or add a proper phone number validation, else it’s worse as is than you not getting the country prefix (something carriers should be able to handle elsewhere anyway ;)).

    Just an example for a simple validation, needs something more solid for all the world:

    add_action('woocommerce_checkout_process', 'wc_validate_nordic_phone');

    function wc_validate_nordic_phone() {

    if (empty($_POST['billing_phone'])) {
    return;
    }

    $raw = trim(wp_unslash($_POST['billing_phone']));

    // Normalize: keep leading + if present, remove everything but digits
    $has_plus = (strpos($raw, '+') === 0);
    $digits = preg_replace('/\D/', '', $raw);

    // Convert 00 prefix to "international" (same treatment as +)
    $is_international = false;
    if ($has_plus) {
    $is_international = true;
    // the digits are already without +
    } elseif (strpos($digits, '00') === 0) {
    $is_international = true;
    $digits = substr($digits, 2); // remove 00
    }

    // Country rules: national length (without country code) for mobile
    // [country code => [min, max]]
    $rules = [
    '47' => [8, 8], // Norway
    '46' => [9, 9], // Sweden (mobile, without leading 0)
    '45' => [8, 8], // Denmark
    '358' => [9, 10], // Finland (mobile varies)
    '354' => [7, 7], // Iceland
    '298' => [6, 6], // Faroe Islands
    '299' => [6, 6], // Greenland
    ];

    $country_code = null;
    $national = $digits;

    if ($is_international) {
    // Find matching country code (longest first to hit 358/354/298/299 before 3/5/2/9)
    $codes = array_keys($rules);
    usort($codes, function ($a, $b) { return strlen($b) - strlen($a); });

    foreach ($codes as $code) {
    if (strpos($digits, $code) === 0) {
    $country_code = $code;
    $national = substr($digits, strlen($code));
    break;
    }
    }

    if ($country_code === null) {
    wc_add_notice(__('Invalid country code. Use +47, +46, +45, +358, +354, +298 eller +299.', 'posten-bring-checkout'), 'error');
    return;
    }
    } else {
    // National format – assume the store's country, fall back to Norway
    $store_country = WC()->customer ? WC()->customer->get_billing_country() : 'NO';
    $country_map = [
    'NO' => '47', 'SE' => '46', 'DK' => '45',
    'FI' => '358', 'IS' => '354', 'FO' => '298', 'GL' => '299',
    ];
    $country_code = $country_map[$store_country] ?? '47';

    // Remove any leading 0 (common in national notation for SE/FI)
    $national = ltrim($national, '0');
    }

    [$min, $max] = $rules[$country_code];
    $len = strlen($national);

    if ($len < $min || $len > $max) {
    wc_add_notice(
    sprintf(
    /* translators: 1: country code, 2: min, 3: max */
    __('Invalid mobile number for +%1$s. Expected %2$d–%3$d digits after the country code.', 'posten-bring-checkout'),
    $country_code, $min, $max
    ),
    'error'
    );
    }
    }
Viewing 15 replies - 1 through 15 (of 18 total)
  • Plugin Author Posten Bring AS

    (@postenbring)

    Hi Rune and thanks for your feedback.

    We know there was problems with the phone number validation similar to what you described in versions up until 1.1.45 where the validation was improved. Could you elaborate on what version you are experiencing this problem with?

    Regards
    Posten Bring Checkout dev-team

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Installed yesterday, so versjon 1.1.51

    Plugin Author Posten Bring AS

    (@postenbring)

    and are you using shortcode or block based checkout?

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    No, and same problem in plain Storefront

    Plugin Author Posten Bring AS

    (@postenbring)

    Interesting – we are not able to reproduce it in our test environment. Validation works as expected in both shortcode and block based checkouts. It should not be possible to proceed without getting a validation error when order button is clicked unless a valid phone number with country code prefix is provided.

    Would it be possible to link to your site so we can examine it?

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Yeah, it’s interesting. I can confirm it seems to work somewhat in a new clean install. Though; t.ex. the error message is only displayed when there is no other validation errors (other fields), and the field is not marked with red – it’s always green (validated). The error message also doesn’t match the others in formatting, nor their normal sorting.

    I can’t share any links here, nor do I actually have time or paid to debug this, so I think I’m just adding an improved custom validation snippet for it, and move on.

    BTW! The missing phone validation might be a JS issue, as it seems like you have added your own validation in JS scripts in this plugin, which might be an issue for sites using caching and optimisations.

    /**
    * Validate Nordic mobile phone numbers on WooCommerce checkout.
    *
    * - Accepts both "+" and "00" international prefixes.
    * - Per-country digit length rules (subscriber number, i.e. after country code).
    * - Falls back to billing_country when no prefix is supplied.
    * - Uses Woo's native error string so translations stay consistent.
    * - Marks the billing_phone field invalid (red border + inline message).
    * - Re-sorts checkout errors to match the field order on the form.
    */
    add_action( 'woocommerce_after_checkout_validation', 'wc_validate_nordic_phone', 10, 2 );

    function wc_validate_nordic_phone( $data, $errors ) {

    if ( empty( $data['billing_phone'] ) ) {
    return; // Let Woo's own "required" validation handle empties.
    }

    // Nordic rules: country code => [ min digits, max digits ] of subscriber number.
    $rules = array(
    '47' => array( 8, 8 ), // Norway
    '45' => array( 8, 8 ), // Denmark
    '46' => array( 9, 9 ), // Sweden (mobile, leading 0 stripped)
    '358' => array( 9, 10 ), // Finland
    '354' => array( 7, 7 ), // Iceland
    '298' => array( 6, 6 ), // Faroe Islands
    '299' => array( 6, 6 ), // Greenland
    );

    // Map billing_country (ISO) to dial code for fallback.
    $country_to_cc = array(
    'NO' => '47', 'DK' => '45', 'SE' => '46',
    'FI' => '358', 'IS' => '354', 'FO' => '298', 'GL' => '299',
    );

    $raw = trim( (string) $data['billing_phone'] );

    // Normalize: keep leading "+" if present, strip everything non-digit.
    $has_plus = ( strpos( $raw, '+' ) === 0 );
    $digits = preg_replace( '/\D+/', '', $raw );

    if ( '' === $digits ) {
    return;
    }

    // Resolve country code from input or fall back to billing_country.
    $cc = '';
    $subscriber = '';

    if ( $has_plus || strpos( $digits, '00' ) === 0 ) {
    // Strip "00" international prefix if present.
    if ( ! $has_plus && strpos( $digits, '00' ) === 0 ) {
    $digits = substr( $digits, 2 );
    }
    // Try matching the longest country code first (3 then 2 digits).
    foreach ( array( 3, 2 ) as $len ) {
    $candidate = substr( $digits, 0, $len );
    if ( isset( $rules[ $candidate ] ) ) {
    $cc = $candidate;
    $subscriber = substr( $digits, $len );
    break;
    }
    }
    } else {
    // No international prefix — use billing_country as the fallback.
    $country = isset( $data['billing_country'] ) ? $data['billing_country'] : '';
    if ( isset( $country_to_cc[ $country ] ) ) {
    $cc = $country_to_cc[ $country ];
    $subscriber = $digits;
    // Allow national trunk "0" prefix (e.g. SE: 070...).
    if ( '46' === $cc && strpos( $subscriber, '0' ) === 0 ) {
    $subscriber = ltrim( $subscriber, '0' );
    }
    }
    }

    // If we can't resolve to a Nordic country, don't block — let other plugins decide.
    if ( '' === $cc || ! isset( $rules[ $cc ] ) ) {
    return;
    }

    list( $min, $max ) = $rules[ $cc ];
    $len = strlen( $subscriber );

    if ( $len < $min || $len > $max ) {

    // Get the field label so the message matches Woo's other field errors.
    $fields = WC()->checkout()->get_checkout_fields( 'billing' );
    $label = isset( $fields['billing_phone']['label'] )
    ? $fields['billing_phone']['label']
    : __( 'Phone', 'woocommerce' );

    // Prefix with "Billing" exactly like WC core (class-wc-checkout.php L866).
    /* translators: %s: field name */
    $label = sprintf( _x( 'Billing %s', 'checkout-validation', 'woocommerce' ), $label );

    // Use Woo's native translation string — translates automatically per locale.
    /* translators: %s: field name */
    $message = sprintf(
    __( '%s is not a valid phone number.', 'woocommerce' ),
    '<strong>' . esc_html( $label ) . '</strong>'
    );

    $errors->add(
    'billing_phone_validation',
    $message,
    array( 'id' => 'billing_phone' )
    );

    // Re-sort all checkout errors to follow the on-screen field order.
    wc_reorder_checkout_errors_by_field_order( $errors );
    }
    }

    /**
    * Reorder a WP_Error bag so checkout field errors appear in the same order
    * as the fields on the form. Errors not tied to a field keep their position
    * at the end.
    */
    function wc_reorder_checkout_errors_by_field_order( $errors ) {

    if ( ! is_wp_error( $errors ) || empty( $errors->errors ) ) {
    return;
    }

    // Build the canonical field order across all checkout fieldsets.
    $checkout = WC()->checkout();
    $all_fieldsets = $checkout->get_checkout_fields();
    $field_order = array();
    $i = 0;
    foreach ( $all_fieldsets as $fieldset ) {
    foreach ( array_keys( $fieldset ) as $field_key ) {
    $field_order[ $field_key ] = $i++;
    }
    }

    // Snapshot current errors with their data, then sort by field position.
    $entries = array();
    foreach ( $errors->errors as $code => $messages ) {
    $error_data = $errors->error_data[ $code ] ?? array();
    $field_id = isset( $error_data['id'] ) ? $error_data['id'] : '';
    $pos = isset( $field_order[ $field_id ] ) ? $field_order[ $field_id ] : PHP_INT_MAX;

    $entries[] = array(
    'code' => $code,
    'messages' => $messages,
    'data' => $error_data,
    'pos' => $pos,
    );
    }

    usort( $entries, function ( $a, $b ) {
    return $a['pos'] <=> $b['pos'];
    } );

    // Rebuild the WP_Error bag in the new order.
    $errors->errors = array();
    $errors->error_data = array();
    foreach ( $entries as $entry ) {
    foreach ( $entry['messages'] as $msg ) {
    $errors->add( $entry['code'], $msg, $entry['data'] );
    }
    }
    }

    /**
    * Mark fields invalid client-side based on server validation errors.
    * Woo emits <li data-id="<field_key>"> for errors that carry an 'id' in their
    * error_data; we use that to add the woocommerce-invalid class on the field
    * wrapper so it gets the red border, matching native field validation.
    */
    add_action( 'wp_footer', 'wc_mark_server_invalid_fields_js', 99 );

    function wc_mark_server_invalid_fields_js() {

    if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
    return;
    }
    ?>
    <script>
    (function ($) {
    $( document.body ).on( 'checkout_error', function () {
    $( '.woocommerce-error li[data-id]' ).each( function () {
    var fieldId = $( this ).data( 'id' );
    if ( ! fieldId ) {
    return;
    }
    var $row = $( '#' + fieldId + '_field' );
    if ( ! $row.length ) {
    return;
    }
    $row
    .removeClass( 'woocommerce-validated' )
    .addClass( 'woocommerce-invalid woocommerce-invalid-phone' );
    } );
    } );
    })( jQuery );
    </script>
    <?php
    }
    • This reply was modified 3 weeks, 2 days ago by Rune Rasmussen.
    • This reply was modified 3 weeks, 2 days ago by threadi.
    Plugin Author Posten Bring AS

    (@postenbring)

    Right – we do observe that the field is not marked correctly in red (which it should), and that the “inline” error message is only displayed with the field when more than one validation error is present in the form. We will fix those, so thanks for pointing that out.

    There is validation in js, but that only applies to block based checkouts which already relies heavily on React.js, so this should not cause any issues with caching/optimisation.

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Block based checkout is probably still not the norm in the Nordic, as there are lot of old stores still using shortcodes. Both because that was what they was built with back in time, but also since several of the available payment plugins still doesn’t support it. Anyhow most optimisation/caching plugins and server side handling most likely would handle core js automatically, but not your extra added js.

    Anyhow your validation isn’t solid enough when customers can add orders without phone number in some setups, whatever the reason is. Nor do I think it’s right of you to manipulate the core checkout fields, even if you can do it with js – also other shipping plugins is mostly working fine without doing so (like Wildrobot and other Posten Bring plugins available). You should probably better handle your country code requirements other places, or in a better way …

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Btw! There’s also an UX issue in this, when country prefix is being pre-filled based on country, as t.ex. Norway (but not only) have plenty of foreign workers and visitors ordering online, using their foreign phone number.

    All in all, not a good solution to manipulate core fields. Betters submit your PR to the core, if you think it needs improvements.

    Plugin Author Posten Bring AS

    (@postenbring)

    Thanks again for the feedback.

    We have identified a couple of UX issues related to how validation errors are presented in some checkout flows, and those will be improved in a future update.

    Regarding the international phone number requirement itself, this is an intentional part of the plugin design. The plugin supports multiple Nordic countries with overlapping numbering formats, and shipment notifications / locker-related flows rely on unambiguous phone numbers in international format.

    This is particularly important for cross-border Nordic shipments. For example, Danish and Norwegian mobile numbers can share the same local number format, which means an incorrectly interpreted number could result in shipment notifications being sent to the wrong recipient. From both a delivery and privacy perspective, we therefore consider it important to collect the number in an explicit international format during checkout.

    Providing the correct phone number at checkout is also important because there is often no reliable way to collect or correct it after the order has been placed and shipment processing has started.

    WooCommerce block checkout is also heavily JS-driven by design, so frontend validation logic in that environment is expected and supported.

    At the moment we have not been able to reproduce a scenario where orders can consistently be completed with invalid phone numbers in a clean installation, but we will continue monitoring feedback and compatibility reports.

    Plugin Author Posten Bring AS

    (@postenbring)

    And btw – we have reported the lack of a proper country code selector as part of the phone number form field several years ago.

    https://github.com/woocommerce/woocommerce/issues/48525

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Thank you for your detailed reply and view, and also for looking into an improving the validation handling. I still don’t agree on this being the best way to deal with it, but I’ll just have to respect that it’s your chosen way to deal with it at the moment. Also I sadly have no time to try to find how and why several customers have been able to checkout without adding the phone number, but hopefully next month if the issue still is there then.

    I looked at that soon two year old and forgotten issue, great initiative. But it seems like this has went into the same black hole as most of the international requirements, Woo is sadly still a bit US orientated in many ways. Anyhow it could probably need some active following up, and some code proposals (PR – Pull Request), to make it more understandable for those deciding about the core features – and pushing it forward.

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    Note! There is one obvious problem with your insertion of country code, like if the store uses other shipping plugins, there is no similar validation in those – they normally use only the Woo logic who accepts t.ex. only +47 as valid. Thus, if you intend to keep this automated insertion of country code, you also need to ensure it’s being validated even if another shipping plugin is selected.

    *The problem might also occur if something goes wrong with the method ID, with your existing validation:

    if (isset($selected_shipping_methods[0]) && str_starts_with($selected_shipping_methods[0], 'posten-bring-checkout')) {
    // all phone validation here
    }
    Plugin Author Posten Bring AS

    (@postenbring)

    The validation will only trigger if the user has selected a shipping option produced by our plugin, since these requirements only apply when shipments are made via Posten Bring Checkout. If another shipping option (possibly from a different plugin) is selected, the special validation will not be triggered. This is to not interfere with different requirements other shipping plugins/providers might have.

    What is the reason you want this to be validated even if another shipping option from a different plugin is selected?

    Thread Starter Rune Rasmussen

    (@syntaxerrorno)

    The reason should be clear from what is already written, but it’s because you have pre-filled the phone field already with t.ex. +47 who then is seen as valid by Woo – and the customers can checkout using other shipping options without filling any phone number beyond the +47 you added …

    Also as written, if something goes wrong with the method id for any reason, even when using your options, +47 will also be seen as a valid input.

    This is to not interfere with different requirements other shipping plugins/providers might have.

    Yes, but still that is exactly what you do at the moment, by pre-filling the phone field for all. 😉

    So if you don’t want to change your validation, you at least need to remove your pre-filled country code when something else is selected – or something goes wrong with the method id.

Viewing 15 replies - 1 through 15 (of 18 total)

You must be logged in to reply to this topic.