Implementing the billing countries object in the Base and Novum themes
This guide outlines how to implement the billing countries object in the Base and Novum Theme Engine themes. This customization will allow you to differentiate when it comes to showing shipping and billing country information
Before you start
Prior to this update, merchants were not able to differentiate when it comes to showing shipping and billing country information. If your store had a setting that defined only a couple of countries to ship to, our code only recognized those same countries as billing options. If the customer's card was from a country that wasn't listed in your shipping zones, it would give the customer an error at checkout even if the billing address was in your shipping zones.
- To implement this customization, you will need access to the Recharge Theme Engine.
- This feature will require advanced HTML, JavaScript, and CSS knowledge. This is not part of Recharge's standard turnkey solution.
- This customization is not supported by Recharge as per the design and integration policy since it requires custom coding.
Step 1 - Amending the code in _addresses.js file
Navigate to the renderAddress() function and change the following.
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() you need to 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 - Amending the code in the _billing.js file
Navigate to the renderBillingAddressHandler() function. There, you would need to 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 - Amending 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 - Amending the code in _script.js file
Navigate to _script.js file and find the Recharge.Form object. There you should 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'];
}
};
After that, update a few more lines.
// 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 - Amending 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;
}
Wrap-up
If you have any questions about the process, feel free to reach out in the #public_theme_edit_v2 Slack channel. We can help clarify the steps and how the end result should look and function.