scormscorm2004scorm1.2lmsscorm-cloud-api

SCORM Compatibility for course hosted on another server


We have a course hosted on our server. We are trying to make it SCORM compatible using the pipwerks API. Initially when we uploaded the course to SCORM Cloud, it led to a cross-domain error. So we tried out the postMessage HTML solution suggested here: Easiest way to convert SCORM package to run on an external server?

Thereafter, we were able to connect and fetch learner details from SCORM Cloud. But unfortunately, we are still unable to update the status and score in SCORM Cloud. At the moment, as soon as we open the course, SCORM Cloud automatically sets the completion and passed status within a few seconds.

This is our first time working with SCORM, so I am not sure if the configuration of the index.html file is right. I am sharing it below.

The console shows the following errors: Console errors

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>LMS</title>

  <!-- Load pipwerks SCORM wrapper (assuming it's hosted) -->
 
  <script src="script.js" defer></script>

  <style>
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
      overflow: hidden;
    }

    #scorm-iframe {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      border: none;
    }
  </style>
</head>
<body>

  <iframe id="scorm-iframe" frameborder="0"></iframe>

  <script>
    let Scormdata = {
      lenname: '',
      lenEmail: '',
      params: 'abc',
      learnerId: 0,
      courseId: 0,
      currentUrl: '',
    };

    const baseUrl = "https://sample.co";
    let dataGet = "";

    const allowedOrigins = [
      "https://sample.co",
      "https://sample.co"
    ];

    // ✅ Message Listener
    window.addEventListener("message", function (event) {
      if (!allowedOrigins.includes(event.origin)) return;

      console.log("📩 Message received:", event.data);

      if (event.data === "FIND_SCORM_API") {
        console.log("📩 SCORM API request received...");

        const scormIframe = document.getElementById("scorm-iframe");
        if (!scormIframe || !scormIframe.contentWindow) {
          console.error("❌ SCORM iframe not found.");
          return;
        }

        const api = pipwerks.SCORM.API;

        // Notify parent that SCORM API is found
        if (event.source && typeof event.source.postMessage === "function") {
          event.source.postMessage(
            { type: "SCORM_API_FOUND", apiAvailable: !!api },
            event.origin
          );
          console.log("✅ Sent SCORM API response to parent.", api);
        } else {
          console.warn("⚠️ Cannot send SCORM API response; event.source missing.");
        }
      }

      // SCORM init response
      if (event.data && event.data.type === "scorm-init-response") {
        console.log("✅ SCORM Init Response:", event.data.success ? "Success" : "Failed");
      }

      // SCORM API response
      if (event.data.type === "SCORM_API_RESPONSE") {
        console.log("✅ SCORM API is available:", event.data.apiAvailable);
      }

      // Handle SCORM Score Update
      if (event.data.type === "SCORM_SCORE_UPDATE") {
        try {
          const score = event.data.score;
          console.log("✅ Score received:", score);

          pipwerks.SCORM.init();
          pipwerks.SCORM.setValue("cmi.score.raw", score);
          pipwerks.SCORM.commit();
          pipwerks.SCORM.finish();

          console.log("✅ Score updated in SCORM Cloud:", score);
        } catch (error) {
          console.error("❌ Error parsing SCORM score data:", error);
        }
      }
    });

    // ✅ Initialize SCORM and send init message to iframe
    function initializeSCORM() {
      const iframe = document.getElementById("scorm-iframe");

      iframe.onload = () => {
        console.log("✅ SCORM iframe loaded. Sending SCORM init request...");
        iframe.contentWindow.postMessage({ type: "scorm-init" }, "*");
      };
    }

    // ✅ Load SCORM learner data and set iframe source
    function loadScormPackage() {
      if (pipwerks.SCORM.init()) {
        const learnerId = pipwerks.SCORM.getValue("cmi.learner_id");
        const learnerName = pipwerks.SCORM.getValue("cmi.learner_name");
        const learnerEmail = pipwerks.SCORM.getValue("cmi.learner_email"); // Optional
        const completionStatus = pipwerks.SCORM.getValue("cmi.completion_status");
        const score = pipwerks.SCORM.getValue("cmi.score.raw");
        const courseId = pipwerks.SCORM.getValue("cmi.entry");

        console.log("Learner ID:", learnerId);
        console.log("Learner Name:", learnerName);
        console.log("Email:", learnerEmail);
        console.log("Completion Status:", completionStatus);
        console.log("Score:", score);
        console.log("Course ID:", courseId);

        const currentUrl = window.location.href;

        if (learnerId && learnerName) {
          Scormdata = {
            ...Scormdata,
            learnerId,
            lenname: learnerName,
            lenEmail: learnerEmail,
            courseId,
            currentUrl
          };

          dataGet = encodeURIComponent(JSON.stringify(Scormdata));
          const fullUrl = baseUrl + dataGet;
          console.log("🌐 Iframe URL:", fullUrl);

          document.getElementById("scorm-iframe").src = fullUrl;
        }
      } else {
        console.error("❌ SCORM API initialization failed.");
      }
    }

    // ✅ On load: initialize SCORM and load data
    window.onload = () => {
      initializeSCORM();
      loadScormPackage();
    };
  </script>
</body>
</html>

Solution

  • No offense, but your code is quite the hodgepodge; it's mixing up standard SCORM syntax with the pipwerks wrapper syntax and trying to invoke a bunch of functions/methods that don't exist.

    To your direct question, the sequence in which you make your calls is vital. The SCORM API is fragile and is not a RESTful API. It doesn't work the way you'd use a modern API in a system like Postman. You connect to it once when you initialize a course session, and you must keep the connection active via JS in your parent window. If you break the API connection, intentionally or otherwise, it will automatically terminate the course session. The code you provided tries to initialize and terminate a SCORM session every time you receive a SCORM_SCORE_UPDATE message. This will fail.

    Your code should create a reference to the API globally (via the SCORM wrapper) then use that reference for the duration of the session. For example:

    //In the course's parent window
    const allowedOrigins = [
      "https://sample.co",
      "https://sample.co"
    ];
    
    let myVars = {
      learner_name: ''
    };
    
    const scorm = pipwerks.scorm;
    
    let success = scorm.init();
    if(success){
       //do stuff like fetch student name
       myVars.learner_name = scorm.get("cmi.core.student_name");
    } else {
       console.log("Course failed to initialize");
       return false; //exit before trying any other stuff since there's no point
    }
    
    //listen to messages from the child iframe. For this example, 
    //listening specifically for the message strings "bookmark" or "exit"
    window.addEventListener("message", function (event) {
    
      if (!allowedOrigins.includes(event.origin)) return;
    
      const data = JSON.parse(event.data);
      switch(Object.keys(data)[0]){
        case "bookmark":
           scorm.set("cmi.core.lesson_location", data[0]);
           scorm.save(); //always save after setting
           break;
        case "exit":
           scorm.quit();
           break;
        // etc.
       }
    
    });
    

    Also:

    I highly recommend creating a course that works without all of the cross-domain stuff, so you can be confident it behaves as you wish. Then add the postMessage abstraction to handle cross-domain scripting.

    Lastly, be sure to pick either SCORM 1.2 or 2004 then stick with it. The syntax and cmi data fields are not interchangable. They also use different manifests.

    Good luck