Integration Guide: Receiving Messages from the Tandem Stance Iframe
When you embed the GOFA Tandem Stance experience as an <iframe>, you can listen for important events and results using the window.postMessage API.
1. Embed the Iframe
<iframe
id="tandemStanceIframe"
src="https://your-gofa-app-url/fall-risk/[fallRiskResultId]/tandem-stance"
width="100%"
height="600"
frameborder="0"
allow="camera; microphone"
></iframe>
2. Listen for Messages
Add a message event listener in your parent page's JavaScript:
window.addEventListener("message", function (event) {
// Optionally, check event.origin for security
// if (event.origin !== 'https://your-gofa-app-url') return;
const { source, type, payload } = event.data || {};
// Only handle messages from the GOFA tandem-stance iframe
if (source !== "gofa-tandem-stance") return;
switch (type) {
case "STATE_TRANSITION":
// payload: { state, metrics }
console.log("State changed:", payload.state, payload.metrics);
break;
case "ASSESSMENT_STARTED":
// payload: { metrics }
console.log("Assessment started:", payload.metrics);
break;
case "ASSESSMENT_ENDED":
// payload: { metrics }
console.log("Assessment ended:", payload.metrics);
break;
case "RESULTS_SET":
// payload: { elapsedSeconds, shoulderTilts }
console.log("Final results:", payload);
break;
case "TIMER_COMPLETE":
console.log("Timer completed");
break;
case "TIMER_ERROR":
// payload: { error }
console.error("Timer error:", payload.error);
break;
default:
// Handle other message types if needed
break;
}
});
3. Message Types and Payloads
STATE_TRANSITION
Fired when the test state changes (e.g., from calibration to introduction to assessment).
Payload:
{
state: "calibration" | "introduction_step1" | "introduction_step2" | "introduction_step3" | "introduction_step4" | "demo" | "ready_showStartPose" | "ready_holdPose" | "ready_holdPose_voiceAssist" | "ready_sequence" | "assessment" | "assessment_complete" | "result_showCheck" | "result_showMetrics" | "paused" | "quited",
metrics: {
elapsedSeconds?: number,
shoulderTilts?: number[] // Array of shoulder tilt measurements collected every second during assessment
}
}
ASSESSMENT_STARTED
Fired when the assessment phase begins.
Payload:
{
metrics: {
elapsedSeconds?: number,
shoulderTilts?: number[] // Array of shoulder tilt measurements collected every second during assessment
}
}
ASSESSMENT_ENDED
Fired when the assessment phase ends. This is especially important as it contains the final assessment results that the parent window needs to capture.
Payload:
{
metrics: {
elapsedSeconds: number, // Final duration the user held the tandem stance (max 30 seconds)
shoulderTilts: number[] // Array of shoulder tilt measurements in degrees, collected every second during the assessment
}
}
RESULTS_SET
Fired when the final assessment results are calculated and stored.
Payload:
{
elapsedSeconds: number, // Final duration the user held the tandem stance (max 30 seconds)
shoulderTilts: number[] // Array of shoulder tilt measurements in degrees, collected every second during the assessment
}
TIMER_COMPLETE
Fired when the assessment timer completes (30 seconds elapsed).
Payload: None
TIMER_ERROR
Fired when there's an error with the assessment timer.
Payload:
{
error: any; // Error details
}
4. Example Implementation
// Track tandem stance test progress
let testResults = null;
let isTestActive = false;
window.addEventListener("message", function (event) {
const { source, type, payload } = event.data || {};
if (source !== "gofa-tandem-stance") return;
switch (type) {
case "STATE_TRANSITION":
console.log("State changed to:", payload.state);
if (payload.state === "assessment") {
isTestActive = true;
updateUI("Tandem stance assessment started");
} else if (payload.state === "assessment_complete") {
isTestActive = false;
updateUI("Assessment completed");
}
break;
case "ASSESSMENT_STARTED":
isTestActive = true;
updateUI("Assessment phase started");
break;
case "ASSESSMENT_ENDED":
isTestActive = false;
testResults = payload.metrics;
updateUI("Assessment phase ended");
console.log("Final assessment data:", testResults);
// This is the most important event - contains complete results
break;
case "RESULTS_SET":
testResults = payload;
showResults(payload);
break;
case "TIMER_COMPLETE":
updateUI("Timer completed");
break;
case "TIMER_ERROR":
console.error("Timer error:", payload.error);
updateUI("Timer error occurred");
break;
}
});
function updateUI(message) {
document.getElementById("status").textContent = message;
}
function showResults(results) {
const avgShoulderTilt =
results.shoulderTilts.length > 0
? (
results.shoulderTilts.reduce((a, b) => a + b, 0) /
results.shoulderTilts.length
).toFixed(1)
: "N/A";
document.getElementById("results").innerHTML = `
<h3>Tandem Stance Test Results</h3>
<p>Duration: ${results.elapsedSeconds} seconds (max 30)</p>
<p>Shoulder Tilt Measurements: ${results.shoulderTilts.length}</p>
<p>Average Shoulder Tilt: ${avgShoulderTilt}°</p>
`;
}
5. Security Considerations
For production environments, always verify the event.origin to ensure messages are coming from your trusted GOFA application:
window.addEventListener("message", function (event) {
// Replace with your actual GOFA app domain
if (event.origin !== "https://your-gofa-app-url") {
console.warn("Received message from untrusted origin:", event.origin);
return;
}
// Process the message...
});