Implementing the billing countries object in the Novum theme Engine

This guide outlines how to implement the billing countries object in the Base and Novum Theme Engine themes. This customization allows you to differentiate between shipping and billing country information.

🚧

Note:

Recharge plans to deprecate the Theme Engine on December 31, 2025. All merchants using Theme Engine must transition to the Affinity customer portal before this date. Review the Transition to Affinity guide for next steps. Note that the upgrade to Affinity is final.



Before you start

To implement this customization, you’ll need:

  • Access to the Recharge Theme Engine
  • Advanced knowledge of HTML, JavaScript, and CSS

Step 1 – Amending the Code in _addresses.js

Navigate to the renderAddress() function and update the following lines:

function renderAddress
// original code
let actionUrl = `{{ shopify_proxy_url if proxy_redirect else '' }}/portal/{{customer.hash}}/addresses`;

// replace with
let actionUrl = attachQueryParams(ReCharge.Endpoints.list_addresses_url());
if proxy_redirect else '' }}/portal/{{customer.hash}}/addresses/${address.id}`;

// original code
let actionUrl = `{{ shopify_proxy_url}}`

// replace with
attachQueryParams(ReCharge.Endpoints.show_addresses_url(address.id));

Navigate to the function renderAddressDetailsHandler(), and change the following.:

function renderAddressDetailsHandler

// original code
getShippingCountries();

// replace with
getShippingBillingCountries('shipping');

After changing renderAddressDetailsHandler() navigate to the function getShippingCountries() and change the following.

// original code
async function getShippingCountries() {
    let shippingCountries = window.Countries || [];

   if (shippingCountries.length > 0) {
       ReCharge.Forms.buildCountries();
       ReCharge.Forms.updateProvinces(document.querySelector("#country"));
   } else {
       try {
           const response = await axios(
               `{{ shopify_proxy_url if proxy_redirect else '' }}/portal/{{ customer.hash }}/request_objects?preview_standard_theme=2&schema={ "shipping_countries": [] }&token=${
                   window.customerToken
               }`
            );

           window.Countries = response.data.shipping_countries;
           ReCharge.Forms.buildCountries();
           ReCharge.Forms.updateProvinces(document.querySelector("#country"));
       } catch (error) {
           console.error(error.response.data.error);
       }
   }
}

// replace with
async function getShippingBillingCountries(type) { 
    let countries = JSON.parse(sessionStorage.getItem('rc_shipping_countries')) || [];

   if (type === 'billing') {
       countries = JSON.parse(sessionStorage.getItem('rc_billing_countries')) || [];
   }

   if (countries.length > 0) {
       ReCharge.Forms.buildCountries(type);
       ReCharge.Forms.updateProvinces(document.querySelector("#country"));
   } else {
       try {
           const schema = `{ "shipping_countries": [], "billing_countries": [] }`;
           const response = await axios({
               url: `${ReCharge.Endpoints.request_objects()}&schema=${schema}`,
               method: "get",
           });

            validateResponseData(response.data, 'countries');

           sessionStorage.setItem('rc_shipping_countries', JSON.stringify(response.data.shipping_countries));
            sessionStorage.setItem('rc_billing_countries', JSON.stringify(response.data.billing_countries));

           ReCharge.Forms.buildCountries(type);
           ReCharge.Forms.updateProvinces(document.querySelector("#country"));
       } catch (error) {
           console.error(error);
       }
   }
}

Find the addAddressHandler() function and change the following lines.

addAddressHandler

//original code
getShippingCountries();

//replace with
getShippingBillingCountries('shipping');

Step 2 - Amend the code in the _billing.js file

Navigate to the renderBillingAddressHandler() function and make the following changes.

function renderBillingAddressHandler

// original code
let actionUrl = `{{ shopify_proxy_url if proxy_redirect else '' }}/portal/{{ customer.hash }}/payment_source/1/address`;
actionUrl = attachQueryParams(actionUrl);

// replace with
let actionUrl = attachQueryParams(ReCharge.Endpoints.update_billing_address());

// original code
getShippingCountries();

// replace with
getShippingBillingCountries('billing');

Step 3 - Amend the code in _helpers.js file

Navigate to the function validateResponseData() and change the following lines of code.

function validateResponseData

// original code
const requiredData = ["products", "orders", "retention_strategies"];

// replace with
let requiredData = ["products", "orders", "retention_strategies"];
 
if (type === 'countries') {
    requiredData = ["shipping_countries", "billing_countries"];
} else if (type === 'charges') {
    requiredData = ["charges", "onetimes"];
}

async function fetchCharges

// on line 1041 add the following code (above this line of code                 
//sessionStorage.setItem('rc_charges', JSON.stringify(response.data.charges));
)

validateResponseData(response.data, 'charges');

Step 4 - Amend the code in _script.js file

Navigate to _script.js file, find the Recharge.Form object and make the following changes:

// original code
ReCharge.Forms = {
    resetErrors: function() {
        document.querySelectorAll('input.error').forEach(function(elem) {
              elem.className = elem.className.replace('error', '');
        });
        document.querySelectorAll('p.error-message').forEach(function(elem) {
              elem.parentNode.removeChild(elem);
        });
    },
    buildCountries: function() {
        if (!window.Countries || !document.querySelector('#country')) { return; }
        var activeCountry = document.querySelector('#country').getAttribute('data-value'),
            options = '<option value="">Please select a country...</option>';
        options +=  window.Countries.map(function(country) {
            var selected = (country.name === activeCountry) ? ' selected' : '';
            return '<option value="' + country.name + '"' + selected + '>' + country.name + '</option>';
        }).join('\n');
        document.querySelector('#country').innerHTML = options;
    },
    showProvinceDropdown: function() {
        if (!document.querySelector('#province') || !document.querySelector('#province_selector')) { return; }
        document.querySelector('#province').setAttribute('style', 'display: none;');
        document.querySelector('#province_selector').setAttribute('style', 'display: inline-block;');
    },
    hideProvinceDropdown: function() {
        if (!document.querySelector('#province') || !document.querySelector('#province_selector')) { return; }
        document.querySelector('#province').setAttribute('style', 'display: inline-block;');
        document.querySelector('#province_selector').setAttribute('style', 'display: none;');
    },
    updateProvinceInput: function(elem) {
        if (!document.querySelector('#province')) { return; }
        document.querySelector('#province').value = elem.value;
    },
    updateProvinces: function(elem) {
        if (!window.Countries || !document.querySelector('#province')) { return; }
        var country = window.Countries.find(function(country) {
            return country.name === elem.value;
        });
        if (!country || !country.provinces.length) {
            window.ReCharge.Forms.hideProvinceDropdown();
            return;
        }
        var provinces = country.provinces,
            activeProvince = document.querySelector('#province').value,
            options = '<option value="">Select province...</option>';
        options +=  provinces.map(function(province) {
            var selected = (province.name === activeProvince) ? ' selected' : '';
            return '<option value="' + province.name + '"' + selected + '>' + province.name + '</option>';
        }).join('\n');
        document.querySelector('#province_selector').innerHTML = options;
        ReCharge.Forms.showProvinceDropdown();
    },
    toggleSubmitButton: function(elem) {
        elem.disabled = !elem.disabled;
        let newText = elem.getAttribute('data-text') || 'Processing... ';
        elem.innerHTML = `<div class="title-bold" style="display: flex; justify-content: center; align-items: center;">${newText} <img src="https://static.rechargecdn.com/static/images/spinner-anim-3.gif?t=1589649332" style="margin-left: 10px; height: 12px;">
    </div> `;
    },
    decodeResponse: function(response) {
        if (typeof(response) === 'string') {
            return response;
        }

        return response['error'] || response['errors'];
    }
};


// replace with
ReCharge.Forms = {
    prettyError: message => {
        message = message.split('_').join(' ');
        return message.charAt(0).toUpperCase() + message.slice(1);
    },
    printError: (form, input, error) => {
        const elementSelector = input == 'general' ? 'button[type="submit"]' : `input[name="${input}"]`;
        const inputElem = form.querySelector(elementSelector);
        const errorMessage = document.createElement('p');
     
        errorMessage.className = 'error-message';
        errorMessage.innerText = ReCharge.Forms.prettyError(error);
     
        try {
            inputElem.className = inputElem.className += ' error';
            inputElem.parentNode.insertBefore(errorMessage, inputElem.nextSibling);
        } catch (e) {
            console.warn(form, input, error, e);
            ReCharge.Toast.addToast('warning', ReCharge.Forms.prettyError(error));
        }
    },
    printAllErrors: (form, errors) => {
        Object.keys(errors).forEach(input => {
            const input_errors = Array.isArray(errors[input]) ? errors[input] : [errors[input]];
            input_errors.forEach(error => {
                ReCharge.Forms.printError(form, input, error);
            });
        });
    },
    updatePropertyElements: (name, value) => {
        document.querySelectorAll(`[data-property="${name}"]`).forEach(elem => elem.innerText = value);
    },
    updateAllProperties: elements => {
        Object.keys(elements).forEach(key => {
            const elem = elements[key];
            ReCharge.Forms.updatePropertyElements(elem.name, elem.value);
        });
    },
    resetErrors: () => {
        document.querySelectorAll('input.error').forEach(elem => {
            elem.className = elem.className.replace('error', '');
        });
        document.querySelectorAll('p.error-message').forEach(elem => {
            elem.parentNode.removeChild(elem);
        });
    },
    buildCountries: function(type = 'shipping') {
      let countries = JSON.parse(sessionStorage.getItem('rc_shipping_countries'));

      if (type === 'billing') {
        countries = JSON.parse(sessionStorage.getItem('rc_billing_countries'));
      }

      if ( !countries.length || !document.querySelector('#country')) { return; }
      var activeCountry = document.querySelector('#country').getAttribute('data-value'),
          options = '<option value="">Please select a country...</option>';
      options += countries.map(function(country) {
        var selected = (country.name === activeCountry) ? ' selected' : '';
        return '<option value="' + country.name + '"' + selected + '>' + country.name + '</option>';
      }).join('\n');
      document.querySelector('#country').innerHTML = options;
    },
    showProvinceDropdown: function() {
      if (!document.querySelector('#province') || !document.querySelector('#province_selector')) { return; }
      document.querySelector('#province').setAttribute('style', 'display: none;');
      document.querySelector('#province_selector').setAttribute('style', 'display: inline-block;');
    },
    hideProvinceDropdown: function() {
      if (!document.querySelector('#province') || !document.querySelector('#province_selector')) { return; }
      document.querySelector('#province').setAttribute('style', 'display: inline-block;');
      document.querySelector('#province_selector').setAttribute('style', 'display: none;');
    },
    updateProvinceInput: function(elem) {
      if (!document.querySelector('#province')) { return; }
      document.querySelector('#province').value = elem.value;
    },
    updateProvinces: function(elem) {
      // replace rc_shipping_countries with rc_billing countries
      const countries = JSON.parse(sessionStorage.getItem('rc_shipping_countries'));

      if (!countries.length || !document.querySelector('#province')) { return; }
      const country = countries.find(function(country) {
        return country.name === elem.value;
      });
      if (!country || !country.provinces.length) {
        window.ReCharge.Forms.hideProvinceDropdown();
        return;
      }
      var provinces = country.provinces,
          activeProvince = document.querySelector('#province').value,
          options = '<option value="">Select province...</option>';
      options +=  provinces.map(function(province) {
        var selected = (province.name === activeProvince) ? ' selected' : '';
        return '<option value="' + province.name + '"' + selected + '>' + province.name + '</option>';
      }).join('\n');
      document.querySelector('#province_selector').innerHTML = options;
      ReCharge.Forms.showProvinceDropdown();
    },
    toggleSubmitButton: function(elem) {
      elem.disabled = !elem.disabled;
      let newText = elem.getAttribute('data-text') || 'Processing... ';
      elem.innerHTML = `<div class="title-bold" style="display: flex; justify-content: center; align-items: center;">${newText} <img src="https://static.rechargecdn.com/static/images/spinner-anim-3.gif?t=1589649332" style="margin-left: 10px; height: 12px;">
      </div> `;    
    },
    decodeResponse: function(response) {
      if (typeof(response) === 'string') {
        return response;
      }

      return response['error'] || response['errors'];
    }
};

Then, update the following:

// on line 169
// original code
list_addresses_url: function() {
    return this.base + `addresses?token=${window.customerToken}&preview_standard_theme=2`;
    },

// replace with
list_addresses_url: function() {
    return this.base + `addresses`;
}


// on line 175
// original code
show_address_url: function(id) {
    return this.base + `addresses/${id}?token=${window.customerToken}&preview_standard_theme=2`;
},

// replace with
show_address_url: function(id) {
    return this.base + `addresses/${id}`;
}

Navigate to the Recharge.Endpoints and add this line: 

// in ReCharge.Endpoints add another route
// code to add
update_billing_address: function() {
    return this.base + `payment_source/1/address`;
}

Change the following:

// on line 409
// original code
$(document).on('submit', 'form[id^="ReChargeForm_"]', async function(evt) {
  evt.preventDefault();
  if (window.locked) { return false; } else { window.locked = true; }
  ReCharge.Forms.resetErrors();
  let $form = $(evt.target);
  let url = $form.attr('action');
  ReCharge.Forms.toggleSubmitButton(evt.target.querySelector('[type="submit"]'));

  let dataUrl = attachQueryParams(url);

  try {
    const response = await axios({
      url: dataUrl,
      method: 'post',
      data: $form.serialize()
    });
    console.log(response.data);
    ReCharge.Toast.addToast('success', 'Updates saved successfully');
    if ($form.find('[name="redirect_url"]').length) {
      const previewParam = sessionStorage.getItem('rc_preview_param');
      const token = window.customerToken;
      let currentUrl = window.location.search;
      let redirectUrl = $form.find('[name="redirect_url"]').val();
      let newUrl;
      if(currentUrl.includes('preview_theme') || currentUrl.includes('preview_standard_theme=2')) {
        newUrl = `${redirectUrl.split('?')[0]}?token=${token}&${previewParam}`;
      } else {
        newUrl = `${redirectUrl.split('?')[0]}?token=${window.customerToken}`;
      }
      window.location.href = newUrl;
    } else {
      window.location.reload();
    }
  } catch(error) {
    console.error(error.response.data.error);
    ReCharge.Forms.toggleSubmitButton(evt.target.querySelector('[type="submit"]'));
    ReCharge.Toast.addToast('error', 'Fix form errors to save updates.');
  } finally {
    delete window.locked;
  }
});

ReCharge.Utils.actionConfirmation();

// replace with
$(document).on('submit', 'form[id^="ReChargeForm_"]', async function(evt) {
  evt.preventDefault();
  if (window.locked) { return false; } else { window.locked = true; }
  ReCharge.Forms.resetErrors();
  let $form = $(evt.target);
  let url = $form.attr('action'); 
  let submitBtn = evt.target.querySelector('[type="submit"]');
  let buttonText = submitBtn.innerText;
   
  ReCharge.Forms.toggleSubmitButton(submitBtn);

  let dataUrl = attachQueryParams(url);

  try {
      const response = await axios({
        url: dataUrl,
        method: 'post',
        data: $form.serialize()
      });
      console.log(response.data);
      ReCharge.Toast.addToast('success', 'Updates saved successfully');
      if ($form.find('[name="redirect_url"]').length) {
        let redirectUrl = $form.find('[name="redirect_url"]').val();
        window.location.href = attachQueryParams(redirectUrl.split('?')[0]);
      } else {
        window.location.reload();
      }
  } catch(errorData) {
      ReCharge.Forms.toggleSubmitButton(submitBtn);
      submitBtn.innerText = buttonText;

      const errors = ReCharge.Forms.decodeResponse(errorData.response.data);  
      console.error('errors', errors);

      if (typeof (errors) === 'object') {
        ReCharge.Forms.printAllErrors(evt.target, errors);
        ReCharge.Toast.addToast('error', 'Fix form errors to save updates.');
      } else {
        ReCharge.Toast.addToast('error', ReCharge.Forms.prettyError(errors));
      }
  } finally {
    delete window.locked;
  }
});

Step 5 - Amend the _styles.css file

Navigate to _styles.css file and add the following CSS properties:

// body#recharge-novum add another property
--color-red: #ec3d11;

body#recharge-novum #recharge-te .error-message,
body#recharge-novum #recharge-te #rc_te-template-wrapper .error-message {
  color: var(--color-red);
  font-size: 11px;
  margin-top: 0;
}