/* Application flows. Submissions go to a Google Form (see ax-config.js) by posting a hidden
form into a hidden iframe — no backend, no keys, no cross-origin read. Google returns an opaque
response, so a completed load is treated as delivered; with no form configured the flow validates
and shows the received state without sending. */
const AX_GFORM = window.AURAXIS_GFORM || {};
const AX_EMAIL = window.AURAXIS_EMAIL || "";
function axGformBase(kind) {
const cfg = AX_GFORM[kind];
return cfg && cfg.formId ? "https://docs.google.com/forms/d/e/" + encodeURIComponent(cfg.formId) : null;
}
/* Prefilled form URL — the guaranteed fallback when the in-page post cannot be confirmed. */
function axGformPrefill(kind, data) {
const base = axGformBase(kind);
if (!base) return null;
const entries = (AX_GFORM[kind] || {}).entries || {};
const q = ["usp=pp_url"];
Object.keys(entries).forEach(key => {
const id = entries[key], v = data[key];
if (!id || v === undefined || v === null || v === "") return;
q.push(encodeURIComponent(id) + "=" + encodeURIComponent(typeof v === "boolean" ? (v ? "Yes" : "No") : String(v)));
});
return base + "/viewform?" + q.join("&");
}
/* Posts a hidden form into a hidden iframe, then verifies that the navigation actually left
this origin. A blocked navigation leaves the sink on about:blank and readable, so it is
reported as "blocked" rather than assumed delivered. */
function axGformPost(kind, data) {
const base = axGformBase(kind);
if (!base) return Promise.resolve("unconfigured");
const entries = (AX_GFORM[kind] || {}).entries || {};
return new Promise(resolve => {
let sink = document.getElementById("ax-gf-sink");
if (!sink) {
sink = document.createElement("iframe");
sink.id = "ax-gf-sink"; sink.name = "ax-gf-sink";
sink.setAttribute("aria-hidden", "true"); sink.tabIndex = -1;
sink.style.display = "none";
document.body.appendChild(sink);
}
const form = document.createElement("form");
form.action = base + "/formResponse";
form.method = "POST"; form.target = "ax-gf-sink"; form.style.display = "none";
Object.keys(entries).forEach(key => {
const id = entries[key], v = data[key];
if (!id || v === undefined || v === null || v === "") return;
const input = document.createElement("input");
input.type = "hidden"; input.name = id;
input.value = typeof v === "boolean" ? (v ? "Yes" : "No") : String(v);
form.appendChild(input);
});
document.body.appendChild(form);
let blocked = false;
const onViolation = e => {
const d = e.effectiveDirective || e.violatedDirective || "";
if (d === "form-action" || d === "frame-src" || d === "child-src") blocked = true;
};
document.addEventListener("securitypolicyviolation", onViolation);
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
sink.removeEventListener("load", finish);
document.removeEventListener("securitypolicyviolation", onViolation);
form.remove();
let left = false;
try {
const href = sink.contentWindow && sink.contentWindow.location.href;
left = !!href && href !== "about:blank";
} catch (_) {
left = true; // cross-origin and unreadable: the navigation genuinely reached Google
}
resolve(!blocked && left ? "sent" : "blocked");
};
sink.addEventListener("load", finish);
setTimeout(finish, 6000);
form.submit();
});
}
function Field({ f, value, onChange, error }) {
const common = { value: value || "", onChange: e => onChange(f.name, e.target.value) };
if (f.type === "select") return (
{error ? {error} : null}
);
if (f.type === "textarea") return (
);
if (f.type === "check") return onChange(f.name, e.target.checked)} />;
return ;
}
function StepStrip({ steps, i }) {
return (
);
}
/* Generic staged flow used by both the Engine application and the consultancy booking. */
function useFlow(steps, kind) {
const [i, setI] = useState(0);
const [data, setData] = useState({});
const [errs, setErrs] = useState({});
const [state, setState] = useState("editing"); // editing | sending | done | failed
const set = useCallback((k, v) => { setData(d => ({ ...d, [k]: v })); setErrs(e => e[k] ? { ...e, [k]: null } : e); }, []);
const validate = () => {
const e = {};
steps[i].fields.forEach(f => {
const v = data[f.name];
if (f.required && (!v || String(v).trim() === "")) e[f.name] = "Required.";
else if (f.type === "email" && v && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v)) e[f.name] = "Address not recognised.";
});
setErrs(e); return Object.keys(e).length === 0;
};
const next = async () => {
if (!validate()) return;
if (i < steps.length - 1) { setI(i + 1); return; }
setState("sending");
let outcome = "blocked";
try { outcome = await axGformPost(kind, data); } catch (_) { outcome = "blocked"; }
setState(outcome === "sent" ? "done" : "failed");
};
return { i, setI, data, set, errs, state, next, setState };
}
function FlowBody({ flow, steps }) {
const { i, data, set, errs } = flow;
const s = steps[i];
return (
{s.heading}
{s.note ?
{s.note}
: null}
{s.fields.map(f => (
))}
);
}
/* Delivery could not be confirmed: never claim receipt. Offer the real form and the desk address. */
function SubmitFallback({ kind, data }) {
const url = axGformPrefill(kind, data);
return (
Not submitted
This submission has not been recorded. Your answers are still in the form below.{url ? " Open the application form to send them, or write to the desk directly." : " Write to the desk directly and we will take it from there."}