Skip to main content

Integration Guide: Receiving Messages from the Single-Leg Stance Iframe

When you embed the GOFA Single-Leg Stance experience as an <iframe>, you can listen for important events and results using the window.postMessage API.

1. Embed the Iframe

<iframe
id="singleLegStanceIframe"
src="https://your-gofa-app-url/fall-risk/[fallRiskResultId]/single-leg-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 single-leg-stance iframe
if (source !== "gofa-single-leg-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 single-leg stance (max 10 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 single-leg stance (max 10 seconds)
shoulderTilts: number[] // Array of shoulder tilt measurements in degrees, collected every second during the assessment
}

TIMER_COMPLETE

Fired when the assessment timer completes (10 seconds elapsed).

Payload: None

TIMER_ERROR

Fired when there's an error with the assessment timer.

Payload:

{
error: any; // Error details
}

EARLY_EXIT

Fired when the user exits the assessment early (e.g., via quit button).

Payload: None

4. Example Implementation

// Track single-leg stance test progress
let testResults = null;
let isTestActive = false;

window.addEventListener("message", function (event) {
const { source, type, payload } = event.data || {};

if (source !== "gofa-single-leg-stance") return;

switch (type) {
case "STATE_TRANSITION":
console.log("State changed to:", payload.state);
if (payload.state === "assessment") {
isTestActive = true;
updateUI("Single-leg 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;

case "EARLY_EXIT":
isTestActive = false;
updateUI("User exited assessment early");
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>Single-Leg Stance Test Results</h3>
<p>Duration: ${results.elapsedSeconds} seconds (max 10)</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...
});