';
el.innerHTML = html;
}
function setStep(n, noScroll) {
state.step = Math.max(0, Math.min(steps.length - 1, n));
renderGauge();
renderSummary();
renderStep();
if (!noScroll) root.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function stepHeader(title, sub) {
var eyebrow = '0' + (state.step + 1) + ' · ' + steps[state.step];
return '
' + esc(eyebrow) + '
' + esc(title) + ' ' + esc(sub) + '
';
}
function renderPlanTable() {
var set = currentSet();
var html = '
';
html += 'Included ';
set.plans.forEach(function (p) {
var isSelected = state.planId === p.id;
html += '' + esc(p.name) + '
';
});
html += ' ';
html += '';
set.plans.forEach(function (p) {
var isSelected = state.planId === p.id;
html += '' +
(p.tag ? '' + esc(p.tag) + '
' : '
') +
'' + fmt(p.monthly) + '/mo
' +
(p.requestFee > 0 ? '' + fmt(p.requestFee) + ' per request
' :
(p.joinFee > 0 ? '+ ' + fmt(p.joinFee) + ' joining fee
' : 'No joining fee
')) +
' ';
});
html += ' ';
html += '';
set.plans.forEach(function (p) {
var isSelected = state.planId === p.id;
html += '' + (isSelected ? '✓ Selected' : 'Choose this plan') + ' ';
});
html += ' ';
set.features.forEach(function (feature, fi) {
html += '' + esc(feature) + ' ';
set.plans.forEach(function (p) {
var isSelected = state.planId === p.id;
html += '' + (p.included[fi] ? '✓ ' : '— ') + ' ';
});
html += ' ';
});
html += '
';
return html;
}
function canContinueStep2() {
var fuelOk = !(state.customerType === 'homeowner' && state.fuel && state.fuel !== 'Mains gas');
return !!(state.boilerMake && state.boilerAge && state.bedrooms && state.fuel && state.existingCustomer && fuelOk);
}
function canContinueStep3() {
return state.fullName.trim().length > 1 && /\S+@\S+\.\S+/.test(state.email) && state.phone.trim().length > 6 && state.addressLine.trim().length > 2;
}
function canContinueStep4() {
return state.accountName.trim().length > 1 && state.sortCode.replace(/\D/g, '').length === 6 && state.accountNumber.replace(/\D/g, '').length >= 6;
}
function renderStep() {
var panel = document.getElementById('wbxStepPanel');
var html = '';
if (state.step === 0) {
html += stepHeader('Choose your cover', 'Tell us who the cover is for, then pick the plan that matches how much protection you want.');
html += '
' +
'I live here ' +
'It\'s a rental I let out ' +
'
';
html += '
' + esc(currentSet().note) + '
';
html += renderPlanTable();
html += '
Continue to coverage check
';
}
else if (state.step === 1) {
html += stepHeader('Check we cover your area', 'Enter your postcode so we can confirm an engineer covers your road and give you an accurate first-visit date.');
html += '
Postcode ' +
'
' +
'
Enter a valid UK postcode.
';
html += '
Check my postcode ';
html += '
' +
(state.coverageChecked ? (state.coverageOk ? 'Good news — engineers cover ' + esc(state.postcode.toUpperCase()) + '.' : 'We don\'t have an engineer covering ' + esc(state.postcode.toUpperCase()) + ' yet. Leave your details on our contact page and we\'ll let you know when we launch there.') : '') + '
';
html += '
Back Continue to property details
';
}
else if (state.step === 2) {
html += stepHeader('Tell us about your boiler', 'This helps us send an engineer who is set up for your system and flag anything that needs a quick look first.');
html += '
';
html += '
Boiler make ' +
'Select make ' +
'Worcester Bosch ' +
'Vaillant ' +
'Baxi ' +
'Ideal ' +
'Viessmann ' +
'Glow-worm ' +
'Not sure ' +
'Other ' +
'
';
html += '
Boiler age ' +
'Select age ' +
'0–2 years ' +
'3–7 years ' +
'8–15 years ' +
'Over 15 years ' +
'
';
html += '
';
html += '
Property size ' +
'Select size ' +
'1–2 bedrooms ' +
'3–4 bedrooms ' +
'5+ bedrooms ' +
'
';
html += '
Fuel type ' +
['Mains gas', 'LPG', 'Oil'].map(function (f) {
return '
' + esc(f) + '
';
}).join('') + '
';
html += '
Homeowner plans currently cover mains gas boilers only. For LPG or oil systems, choose a landlord plan or contact WarmBox directly.
';
html += '
Have we serviced or installed this boiler before? ' +
['Yes', 'No', 'Not sure'].map(function (f) {
return '
' + esc(f) + '
';
}).join('') + '
';
html += '
Back Continue to your details
';
}
else if (state.step === 3) {
html += stepHeader('Your details', 'We\'ll use this to confirm your membership and get an engineer booked in.');
html += '
Full name
';
html += '
';
html += '
';
html += '
Back Continue to payment
';
}
else if (state.step === 4) {
html += stepHeader('Set up your Direct Debit', 'Membership payments are collected by Direct Debit through GoCardless, so your plan renews automatically with no card details to update.');
html += '
Name on the account
';
html += '
';
html += '
The Direct Debit Guarantee. This Guarantee is offered by all banks and building societies that accept instructions to pay Direct Debits. If there\'s ever a change to the amount or date of your payment, WarmBox will notify you in advance. If an error is made, you\'re entitled to a full and immediate refund from your bank or building society.
';
html += '
Back Continue to confirm
';
}
else if (state.step === 5) {
var p = getPlan();
html += stepHeader('Confirm your membership', 'Check the details below, then confirm to activate your cover.');
html += '
';
html += '
Membership type ' + esc(currentSet().label) + '
';
html += '
Plan ' + esc(p.name + (p.tag ? ' · ' + p.tag : '')) + '
';
html += '
Monthly payment ' + fmt(p.monthly) + '
';
if (p.requestFee > 0) html += '
Fee per engineer request ' + fmt(p.requestFee) + ' (charged only when you use it)
';
else if (p.joinFee > 0) html += '
Joining fee (due today) ' + fmt(p.joinFee) + '
';
html += '
Property ' + esc(state.postcode.toUpperCase()) + '
';
html += '
Contact ' + esc(state.fullName) + '
';
html += '
';
var termsUrl = CONFIG.termsUrl || '#';
html += '
';
html += '
I\'ve read and agree to the WarmBox membership terms and exclusions for this plan.
';
html += '
I understand I have a 14-day cooling-off period from today to cancel this membership for a full refund.
';
if (state.submitError) html += '
' + esc(state.submitError) + '
';
html += '
Back ' + (state.submitting ? 'Sending…' : 'Confirm my membership') + '
';
}
else if (state.step === 6) {
var finalPlan = getPlan();
html += '
';
html += '
';
html += '
You\'re covered ';
html += '
Your ' + esc(finalPlan ? finalPlan.name + (finalPlan.tag ? ' (' + finalPlan.tag + ')' : '') : 'WarmBox') + ' membership is active from today.
';
html += '
' + esc(state.reference || '') + '
';
html += '
';
html += '
' +
'A confirmation email is on its way to ' + esc(state.email || 'your inbox') + ' with your membership pack. ' +
(finalPlan && finalPlan.requestFee > 0 ? 'This plan has no joining fee — instead, a ' + fmt(finalPlan.requestFee) + ' fee applies each time you request an engineer visit. ' : (finalPlan && finalPlan.joinFee > 0 ? 'Your one-off joining fee of ' + fmt(finalPlan.joinFee) + ' will be collected with your first payment. ' : '')) +
'Your Direct Debit mandate is being set up now — this usually takes 2–3 working days to confirm with your bank. ' +
'We\'ll text you a link to pick a date for your first visit once the mandate is confirmed. ' +
'You can cancel free of charge within 14 days, or any time after with no exit fees. ' +
' ';
html += '
Start another quote
';
}
panel.innerHTML = html;
bindStepEvents();
}
function refreshNextDisabled(fn) {
var btn = document.getElementById('wbxNextBtn');
if (btn) btn.disabled = !fn();
}
function activatePills(selector, attribute, handler) {
root.querySelectorAll(selector).forEach(function (pill) {
function activate() { handler(pill.getAttribute(attribute)); }
pill.addEventListener('click', activate);
pill.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activate();
}
});
});
}
function bindStepEvents() {
var backBtn = document.getElementById('wbxBackBtn');
if (backBtn) backBtn.onclick = function () { if (!state.submitting) setStep(state.step - 1); };
var nextBtn = document.getElementById('wbxNextBtn');
if (nextBtn) nextBtn.onclick = function () { if (!nextBtn.disabled) setStep(state.step + 1); };
var restartBtn = document.getElementById('wbxRestartBtn');
if (restartBtn) restartBtn.onclick = function () {
state = {
step: 0, customerType: 'homeowner', planId: null, postcode: '', coverageChecked: false, coverageOk: null,
boilerAge: '', boilerMake: '', fuel: '', bedrooms: '', existingCustomer: '', fullName: '', email: '', phone: '',
addressLine: '', accountName: '', sortCode: '', accountNumber: '', agreeTerms: false, agreeCoolingOff: false, reference: null, submitting: false, submitError: ''
};
setStep(0);
};
if (state.step === 0) {
root.querySelectorAll('#wbxAudienceToggle button').forEach(function (b) {
b.onclick = function () {
state.customerType = b.getAttribute('data-a');
state.planId = null;
renderSummary();
renderStep();
};
});
root.querySelectorAll('[data-choose]').forEach(function (btn) {
btn.onclick = function () {
state.planId = btn.getAttribute('data-choose');
renderSummary();
renderStep();
};
});
}
if (state.step === 1) {
var postcodeInput = document.getElementById('wbxPostcodeInput');
postcodeInput.oninput = function () {
state.postcode = postcodeInput.value;
state.coverageChecked = false;
state.coverageOk = null;
refreshNextDisabled(function () { return false; });
};
document.getElementById('wbxCheckCoverageBtn').onclick = function () {
var val = postcodeInput.value.trim();
var valid = /^[A-Za-z]{1,2}\d[A-Za-z\d]?\s*\d[A-Za-z]{2}$/.test(val);
var err = document.getElementById('wbxPostcodeError');
var result = document.getElementById('wbxCoverageResult');
if (!valid) {
err.className = 'wbx-field-error show';
return;
}
err.className = 'wbx-field-error';
state.postcode = val;
state.coverageChecked = true;
state.coverageOk = !/^zz/i.test(val.trim());
if (state.coverageOk) {
result.className = 'wbx-coverage-result show ok';
result.textContent = 'Good news — engineers cover ' + val.toUpperCase() + '.';
} else {
result.className = 'wbx-coverage-result show no';
result.textContent = 'We don\'t have an engineer covering ' + val.toUpperCase() + ' yet. Leave your details on our contact page and we\'ll let you know when we launch there.';
}
renderSummary();
refreshNextDisabled(function () { return !!state.coverageOk; });
};
}
if (state.step === 2) {
document.getElementById('wbxBoilerMake').onchange = function (e) { state.boilerMake = e.target.value; refreshNextDisabled(canContinueStep2); };
document.getElementById('wbxBoilerAge').onchange = function (e) { state.boilerAge = e.target.value; refreshNextDisabled(canContinueStep2); };
document.getElementById('wbxBedrooms').onchange = function (e) { state.bedrooms = e.target.value; refreshNextDisabled(canContinueStep2); };
activatePills('#wbxFuelGroup .wbx-radio-pill', 'data-fuel', function (value) { state.fuel = value; renderSummary(); renderStep(); });
activatePills('#wbxExistingGroup .wbx-radio-pill', 'data-existing', function (value) { state.existingCustomer = value; renderStep(); });
}
if (state.step === 3) {
var map = {
wbxFullName: 'fullName',
wbxEmail: 'email',
wbxPhone: 'phone',
wbxAddressLine: 'addressLine'
};
Object.keys(map).forEach(function (id) {
var el = document.getElementById(id);
el.oninput = function () {
state[map[id]] = el.value;
refreshNextDisabled(canContinueStep3);
renderSummary();
};
});
}
if (state.step === 4) {
['wbxAccountName','wbxSortCode','wbxAccountNumber'].forEach(function (id) {
var el = document.getElementById(id);
var key = id === 'wbxAccountName' ? 'accountName' : (id === 'wbxSortCode' ? 'sortCode' : 'accountNumber');
el.oninput = function () {
state[key] = el.value;
refreshNextDisabled(canContinueStep4);
};
});
}
if (state.step === 5) {
document.getElementById('wbxAgreeTerms').onchange = function (e) {
state.agreeTerms = e.target.checked;
renderStep();
};
document.getElementById('wbxAgreeCoolingOff').onchange = function (e) {
state.agreeCoolingOff = e.target.checked;
renderStep();
};
var submitBtn = document.getElementById('wbxSubmitBtn');
if (submitBtn) submitBtn.onclick = submitToGravityForms;
}
}
var GRAVITY_FORM_ID = 35;
function getWordPressRestRoot() {
var link = document.querySelector('link[rel="https://api.w.org/"]');
if (link && link.href) return link.href.replace(/\/$/, '');
return window.location.origin.replace(/\/$/, '') + '/wp-json';
}
function buildGravityPayload() {
var p = getPlan();
if (!p) throw new Error('No WarmBox plan is selected.');
var fee = p.requestFee > 0
? fmt(p.requestFee) + ' per engineer request'
: (p.joinFee > 0 ? fmt(p.joinFee) + ' joining fee' : 'None');
var payload = {
input_1: currentSet().label,
input_4: p.name,
input_5: p.tag || 'Standard',
input_6: fmt(p.monthly) + ' per month',
input_7: fee,
input_8: p.id,
input_10: state.postcode.toUpperCase(),
input_11: state.coverageOk ? 'Covered' : 'Not covered',
input_13: state.boilerMake,
input_14: state.boilerAge,
input_15: state.bedrooms,
input_16: state.fuel,
input_17: state.existingCustomer,
input_19: state.fullName.trim(),
input_20: state.email.trim(),
input_21: state.phone.trim(),
input_22: state.addressLine.trim(),
input_24: state.accountName.trim(),
input_25: state.sortCode.trim(),
input_26: state.accountNumber.trim(),
input_30_1: state.agreeTerms ? 'terms_accepted' : '',
input_31_1: state.agreeCoolingOff ? 'cooling_off_acknowledged' : '',
input_32: state.reference,
input_33: 'Direct Debit details received - mandate setup pending',
input_34: 'WarmBox single Elementor HTML block',
source_page: 6,
target_page: 0
};
if (state.customerType === 'homeowner') payload.input_2 = p.id;
else payload.input_3 = p.id;
return payload;
}
function friendlyValidationMessage(messages) {
if (!messages) return '';
var labels = {
'1': 'Membership type',
'2': 'Homeowner plan',
'3': 'Landlord plan',
'10': 'Postcode',
'13': 'Boiler make',
'14': 'Boiler age',
'15': 'Property size',
'16': 'Fuel type',
'17': 'Previous WarmBox service/install',
'19': 'Full name',
'20': 'Email',
'21': 'Phone',
'22': 'Address',
'24': 'Account holder name',
'25': 'Sort code',
'26': 'Account number',
'30': 'Terms acceptance',
'31': 'Cooling-off acknowledgement'
};
var parts = [];
Object.keys(messages).forEach(function (id) {
var msg = messages[id];
if (Array.isArray(msg)) msg = msg.join(' ');
parts.push((labels[id] ? labels[id] + ': ' : '') + String(msg));
});
return parts.join(' ');
}
function gravityEndpoint() {
return getWordPressRestRoot() + '/gf/v2/forms/' + GRAVITY_FORM_ID + '/submissions';
}
function encodeGravityPayload(payload) {
var body = new URLSearchParams();
Object.keys(payload).forEach(function (key) {
var value = payload[key];
if (value === null || typeof value === 'undefined') value = '';
body.append(key, String(value));
});
return body.toString();
}
async function postGravitySubmission(payload) {
var endpoint = gravityEndpoint();
var response;
try {
response = await fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: encodeGravityPayload(payload)
});
} catch (networkError) {
throw new Error('Could not reach Gravity Forms. Please try again.');
}
var raw = await response.text();
var data = null;
try { data = raw ? JSON.parse(raw) : null; } catch (parseError) {}
if (response.status === 404) {
throw new Error('Gravity Forms REST API is disabled on this site. In WordPress go to Forms > Settings > REST API, tick Enabled, then click Update.');
}
if (response.status === 401 || response.status === 403) {
throw new Error('The server blocked the Gravity Forms submission endpoint (HTTP ' + response.status + '). Check any security/firewall rule blocking /wp-json/gf/v2/.');
}
if (!response.ok) {
var apiMessage = data && (data.message || (data.data && data.data.message));
throw new Error(apiMessage || ('Gravity Forms returned HTTP ' + response.status + '.'));
}
if (!data) {
throw new Error('Gravity Forms returned a non-JSON response. Check the Gravity Forms REST API setting and any cache/security plugin affecting /wp-json/.');
}
if (data.is_valid !== true) {
var validation = friendlyValidationMessage(data.validation_messages);
throw new Error(validation || 'Gravity Forms rejected the application.');
}
return data;
}
async function submitToGravityForms() {
if (state.submitting || !state.agreeTerms || !state.agreeCoolingOff) return;
state.submitError = '';
state.reference = state.reference || ('WBX' + Math.floor(1000000 + Math.random() * 9000000));
state.submitting = true;
renderStep();
try {
var payload = buildGravityPayload();
var result = await postGravitySubmission(payload);
if (!result || result.is_valid !== true) {
throw new Error('Gravity Forms did not confirm the application.');
}
state.submitting = false;
state.submitError = '';
setStep(6);
} catch (err) {
console.error('WarmBox submission failed:', err);
state.submitting = false;
state.submitError = err && err.message
? err.message
: 'The application could not be submitted. Please try again.';
renderStep();
}
}
renderGauge();
renderSummary();
renderStep();
})();