diff --git a/lms_xapi.module b/lms_xapi.module
index 32d48eadd54e3c950d24297991ef090d810acb03..177f274963aa1a8107ce55fcb90b719f8d5d5fcc 100644
--- a/lms_xapi.module
+++ b/lms_xapi.module
@@ -9,6 +9,7 @@ declare(strict_types=1);
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\lms_xapi\LRSReferenceStorage;
 use Drupal\user\UserInterface;
 
 /**
@@ -20,7 +21,7 @@ function lms_xapi_entity_delete(EntityInterface $entity) {
   }
 
   // Cleanup.
-  $storage = \Drupal::service('lms_xapi.lrs_reference_storage');
+  $storage = \Drupal::service(LRSReferenceStorage::class);
   if ($entity instanceof UserInterface) {
     // LMS IDs always start with a user ID.
     $storage->deleteLrsReferences($entity->id() . ':%');
diff --git a/lms_xapi.services.yml b/lms_xapi.services.yml
index 9c53ebe511064a5cc5aab077007945f366f64b18..c405323a334ecb34268bd49c59daabeebc8bb253 100644
--- a/lms_xapi.services.yml
+++ b/lms_xapi.services.yml
@@ -2,10 +2,10 @@ services:
   _defaults:
     autowire: true
 
-  lms_xapi.lrs_reference_storage:
-    class: 'Drupal\lms_xapi\LRSReferenceStorage'
-  Drupal\lms_xapi\LRSReferenceStorage: '@lms_xapi.lrs_reference_storage'
+  Drupal\lms_xapi\LRSReferenceStorage: ~
 
-  lms_xapi.tincan:
-    class: 'Drupal\lms_xapi\TincanService'
-  Drupal\lms_xapi\TincanService: '@lms_xapi.tincan'
+  Drupal\lms_xapi\XapiService: ~
+
+  logger.channel.lms_xapi:
+    parent: logger.channel_base
+    arguments: ['lms_xapi']
diff --git a/modules/lms_xapi_activity/src/ActivityXapiIdGenerator.php b/modules/lms_xapi_activity/src/ActivityXapiIdGenerator.php
index 3082a127fddd210de8b59b7518e67b328e7b9612..662077e9e97810b8d5c3b83790b0f714a5c0be35 100644
--- a/modules/lms_xapi_activity/src/ActivityXapiIdGenerator.php
+++ b/modules/lms_xapi_activity/src/ActivityXapiIdGenerator.php
@@ -11,8 +11,10 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\lms\Entity\Bundle\Course;
 use Drupal\lms\TrainingManager;
 use Drupal\lms_xapi\XapiIdGeneratorInterface;
-use Drupal\lms\Entity\Activity;
+use Drupal\lms\Entity\ActivityInterface;
 use Drupal\lms\Entity\ActivityTypeInterface;
+use Drupal\lms\Entity\AnswerInterface;
+use Drupal\lms\Entity\CourseStatusInterface;
 
 /**
  * Xapi LRS event subscriber.
@@ -35,20 +37,15 @@ class ActivityXapiIdGenerator implements XapiIdGeneratorInterface {
     AccountInterface $student,
     CacheableMetadata $cacheable_metadata,
   ): bool {
-    if (!$entity instanceof Activity) {
+    if (!$entity instanceof ActivityInterface) {
       return FALSE;
     }
-    $course = $this->routeMatch->getParameter('group');
     $cacheable_metadata->setCacheContexts($cacheable_metadata->getCacheContexts() + ['route']);
-    if ($course instanceof Course) {
-      $course_status = $this->trainingManager->loadCourseStatus($course, $student, [
-        'current' => TRUE,
-      ]);
-      $cacheable_metadata->addCacheableDependency($course_status);
-      if ($course_status === NULL) {
-        return FALSE;
-      }
+    $course_status = $this->getCourseStatus($student);
+    if ($course_status === NULL) {
+      return FALSE;
     }
+    $cacheable_metadata->addCacheableDependency($course_status);
 
     $bundle = $entity->get('type')->entity;
     assert($bundle instanceof ActivityTypeInterface);
@@ -68,11 +65,10 @@ class ActivityXapiIdGenerator implements XapiIdGeneratorInterface {
     AccountInterface $student,
     CacheableMetadata $cacheable_metadata,
   ): array {
-
-    $course = $this->routeMatch->getParameter('group');
-    $course_status = $this->trainingManager->loadCourseStatus($course, $student, [
-      'current' => TRUE,
-    ]);
+    $course_status = $this->getCourseStatus($student);
+    if ($course_status === NULL) {
+      throw new \Exception('Unable to determine the current course status.');
+    }
 
     return [
       $entity->getEntityTypeId(),
@@ -81,4 +77,24 @@ class ActivityXapiIdGenerator implements XapiIdGeneratorInterface {
     ];
   }
 
+  /**
+   * Get the current course status from route.
+   */
+  private function getCourseStatus(AccountInterface $student): ?CourseStatusInterface {
+    $course = $this->routeMatch->getParameter('group');
+    if (!$course instanceof Course) {
+      // This may be an answer details route.
+      $answer = $this->routeMatch->getParameter('lms_answer');
+      if (!$answer instanceof AnswerInterface) {
+        return NULL;
+      }
+      return $answer->getLessonStatus()->getCourseStatus();
+    }
+    else {
+      return $this->trainingManager->loadCourseStatus($course, $student, [
+        'current' => TRUE,
+      ]);
+    }
+  }
+
 }
diff --git a/modules/lms_xapi_activity/src/Plugin/ActivityAnswer/Xapi.php b/modules/lms_xapi_activity/src/Plugin/ActivityAnswer/Xapi.php
index 71f9a86fa9a3df8eb332ef88b5cfae1d6bc9e151..ea8fd3f431df35ad6d209950d56c5d45d4bb3b34 100644
--- a/modules/lms_xapi_activity/src/Plugin/ActivityAnswer/Xapi.php
+++ b/modules/lms_xapi_activity/src/Plugin/ActivityAnswer/Xapi.php
@@ -9,7 +9,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\lms\Attribute\ActivityAnswer;
 use Drupal\lms\Entity\Answer;
 use Drupal\lms\Plugin\ActivityAnswerBase;
-use Drupal\lms_xapi\TincanService;
+use Drupal\lms_xapi\XapiService;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -22,35 +22,90 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 class Xapi extends ActivityAnswerBase {
 
   /**
-   * The TinCan service.
+   * The Xapi service.
    */
-  protected TinCanService $lrsService;
+  protected XapiService $xapi;
+
+  /**
+   * Static score storage.
+   */
+  private ?float $score = NULL;
 
   /**
    * {@inheritdoc}
    */
   public function injectServices(ContainerInterface $container): void {
     parent::injectServices($container);
-    $this->lrsService = $container->get('lms_xapi.tincan');
+    $this->xapi = $container->get(XapiService::class);
   }
 
   /**
    * {@inheritdoc}
    */
   public function getScore(Answer $answer): float {
+    if ($this->score !== NULL) {
+      return $this->score;
+    }
+
     $account = $answer->getOwner();
     $activity = $answer->getActivity();
     $cacheable_metadata = new CacheableMetadata();
-    $lms_id = $this->lrsService->getLmsId($activity, $account, $cacheable_metadata);
-    $score = $this->lrsService->getScoreFromLrs($lms_id);
-    return $score ?? 0;
+    $lms_id = $this->xapi->getLmsId($activity, $account, $cacheable_metadata);
+    $score = $this->xapi->getScoreFromLrs($lms_id);
+    $this->score = $score ?? 0;
+
+    return $this->score;
   }
 
   /**
    * {@inheritdoc}
    */
   public function evaluatedOnSave(Answer $answer): bool {
+    if ($this->getScore($answer) === 0.0) {
+      return FALSE;
+    }
     return TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function evaluationDisplay(Answer $answer): array {
+    $activity = $answer->getActivity();
+
+    if (!$activity->hasField('field_xapi_package')) {
+      throw new \Exception('Xapi field missing on activity.');
+    }
+    $items = $activity->get('field_xapi_package');
+    if ($items->isEmpty()) {
+      return [];
+    }
+    /** @var \Drupal\lms_xapi\Plugin\Field\FieldType\XapiItem */
+    $xapi_item = $items->first();
+    $cacheable_metadata = new CacheableMetadata();
+    $launch_url = $this->xapi->getLaunchUrl(
+      $xapi_item->getPackagePath(),
+      $activity,
+      $answer->get('user_id')->entity,
+      $cacheable_metadata
+    );
+    if ($launch_url === NULL) {
+      return [];
+    }
+
+    // Display the entire package with student data loaded from LRS.
+    $build = [
+      '#type' => 'html_tag',
+      '#tag' => 'iframe',
+      '#attributes' => [
+        'src' => $launch_url,
+        'style' => 'width: 100%; min-height: 800px; border: 0;',
+      ],
+      '#value' => '',
+    ];
+    $cacheable_metadata->applyTo($build);
+
+    return $build;
+  }
+
 }
diff --git a/modules/lms_xapi_lesson/src/Form/XapiLessonForm.php b/modules/lms_xapi_lesson/src/Form/XapiLessonForm.php
index caeeb9c668905b45bc0f5e34be260dd307afad44..5f8c3e8053f7b1263c1f9936363c838c458608fe 100644
--- a/modules/lms_xapi_lesson/src/Form/XapiLessonForm.php
+++ b/modules/lms_xapi_lesson/src/Form/XapiLessonForm.php
@@ -9,7 +9,7 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\lms\Entity\LessonStatusInterface;
 use Drupal\lms\Form\AnswerFormTrait;
 use Drupal\lms\TrainingManager;
-use Drupal\lms_xapi\TincanService;
+use Drupal\lms_xapi\XapiService;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -24,7 +24,7 @@ final class XapiLessonForm extends FormBase {
    */
   public function __construct(
     private readonly TrainingManager $trainingManager,
-    private readonly TincanService $tincan,
+    private readonly XapiService $xapi,
   ) {}
 
   /**
@@ -33,7 +33,7 @@ final class XapiLessonForm extends FormBase {
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('lms.training_manager'),
-      $container->get('lms_xapi.tincan')
+      $container->get(XapiService::class)
     );
   }
 
@@ -74,7 +74,7 @@ final class XapiLessonForm extends FormBase {
 
     // Update lesson score from LRS.
     $lesson = $lesson_status->getLesson();
-    $lrs_score = $this->tincan->getScoreFromLrs(
+    $lrs_score = $this->xapi->getScoreFromLrs(
       $lesson_status->getCourseStatus()->getUserId(),
       'lms_lesson',
       $lesson->id()
diff --git a/modules/lrs_xapi/src/Controller/LrsXapiEndpoint.php b/modules/lrs_xapi/src/Controller/LrsXapiEndpoint.php
index 334d36df517172fcb4c62547b0b31de20e4f4e29..85d6c955eae9265512746c27e429e1bdfa764990 100644
--- a/modules/lrs_xapi/src/Controller/LrsXapiEndpoint.php
+++ b/modules/lrs_xapi/src/Controller/LrsXapiEndpoint.php
@@ -84,7 +84,6 @@ final class LrsXapiEndpoint implements ContainerInjectionInterface {
    */
   public function statements(Request $request): Response {
     $this->authorize($request);
-
     if ($request->getMethod() === 'PUT') {
       $statement_data = $request->getContent();
       $this->statementStorage->setStatement($statement_data);
@@ -112,7 +111,7 @@ final class LrsXapiEndpoint implements ContainerInjectionInterface {
     else {
       $this->logRequest($request);
     }
-    return new Response('OK');
+    return new Response('OK', 200);
   }
 
   /**
diff --git a/src/Form/LmsXapiSettingsForm.php b/src/Form/LmsXapiSettingsForm.php
index c23e590d14aebe864929e1534126659969984290..08b9c1fa4104194a31e17252ac77d756a611d364 100644
--- a/src/Form/LmsXapiSettingsForm.php
+++ b/src/Form/LmsXapiSettingsForm.php
@@ -7,7 +7,7 @@ namespace Drupal\lms_xapi\Form;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\ConfigTarget;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\lms_xapi\TincanService;
+use Drupal\lms_xapi\XapiService;
 
 /**
  * LMS Xapi settings form class.
@@ -25,7 +25,7 @@ final class LmsXapiSettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   protected function getEditableConfigNames() {
-    return [TincanService::CONFIG_NAME];
+    return [XapiService::CONFIG_NAME];
   }
 
   /**
@@ -35,19 +35,19 @@ final class LmsXapiSettingsForm extends ConfigFormBase {
     $form['endpoint'] = [
       '#type' => 'textfield',
       '#title' => $this->t('LRS API endpoint'),
-      '#config_target' => new ConfigTarget(TincanService::CONFIG_NAME, 'endpoint'),
+      '#config_target' => new ConfigTarget(XapiService::CONFIG_NAME, 'endpoint'),
       '#required' => TRUE,
     ];
     $form['username'] = [
       '#type' => 'textfield',
       '#title' => $this->t('LRS API user name'),
-      '#config_target' => new ConfigTarget(TincanService::CONFIG_NAME, 'username'),
+      '#config_target' => new ConfigTarget(XapiService::CONFIG_NAME, 'username'),
       '#required' => TRUE,
     ];
     $form['password'] = [
       '#type' => 'password',
       '#title' => $this->t('LRS API password'),
-      '#config_target' => new ConfigTarget(TincanService::CONFIG_NAME, 'password'),
+      '#config_target' => new ConfigTarget(XapiService::CONFIG_NAME, 'password'),
       '#required' => TRUE,
     ];
 
diff --git a/src/Plugin/Field/FieldFormatter/XapiFieldFormatter.php b/src/Plugin/Field/FieldFormatter/XapiFieldFormatter.php
index 31f9120c179a16dd219a4b78b667f5c286867f38..84848e9d42b381504bcf03e74f06239a42b7e82d 100644
--- a/src/Plugin/Field/FieldFormatter/XapiFieldFormatter.php
+++ b/src/Plugin/Field/FieldFormatter/XapiFieldFormatter.php
@@ -4,20 +4,15 @@ declare(strict_types=1);
 
 namespace Drupal\lms_xapi\Plugin\Field\FieldFormatter;
 
-use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Field\Attribute\FieldFormatter;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\FormatterBase;
-use Drupal\Core\File\FileUrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Site\Settings;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\Url;
 use Drupal\lms_xapi\Plugin\Field\FieldType\XapiItem;
-use Drupal\lms_xapi\TincanService;
+use Drupal\lms_xapi\XapiService;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -42,10 +37,8 @@ final class XapiFieldFormatter extends FormatterBase {
     $label,
     $view_mode,
     array $third_party_settings,
-    protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
-    protected readonly ImmutableConfig $config,
     protected readonly AccountInterface $currentUser,
-    protected readonly TincanService $tincan,
+    protected readonly XapiService $xapi,
   ) {
     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
   }
@@ -55,10 +48,8 @@ final class XapiFieldFormatter extends FormatterBase {
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['label'], $configuration['view_mode'], $configuration['third_party_settings'],
-      $container->get('file_url_generator'),
-      $container->get('config.factory')->get('lms_xapi.settings'),
-      $container->get('current_user'),
-      $container->get('lms_xapi.tincan'),
+      $container->get(AccountInterface::class),
+      $container->get(XapiService::class),
     );
   }
 
@@ -79,54 +70,17 @@ final class XapiFieldFormatter extends FormatterBase {
       return $no_cache_return;
     }
 
-    $path = $item->getPackagePath();
-    $tincan_file = $path . '/tincan.xml';
-    if (!\file_exists($tincan_file)) {
-      return $no_cache_return;
-    }
-
-    $config = [
-      'endpoint' => $this->config->get('endpoint'),
-    ];
-    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
-      $config += [
-        'username' => $this->config->get('username'),
-        'password' => $this->config->get('password'),
-      ];
-    }
-    foreach ($config as $value) {
-      if ($value === NULL || $value === '') {
-        return $no_cache_return;
-      }
-    }
-
-    $entity = $item->getEntity();
-
-    $xml = new \SimpleXMLElement(\file_get_contents($tincan_file));
-    $launch_file = $path . '/' . $xml->activities->activity->launch;
-    $launch_url = $this->fileUrlGenerator->generateAbsoluteString($launch_file);
-
-    // Support relative URLs.
-    if (!\str_starts_with($config['endpoint'], 'http')) {
-      $config['endpoint'] = Url::fromRoute('<front>', [], [
-        'absolute' => TRUE,
-      ])->toString() . $config['endpoint'];
-    }
     $cacheable_metadata = new CacheableMetadata();
-    $lms_id = $this->tincan->getLmsId($entity, $this->currentUser, $cacheable_metadata);
 
-    $query_args = [
-      'endpoint' => $config['endpoint'],
-      'actor' => Json::encode([
-        'mbox_sha1sum' => sha1('mailto:' . $this->currentUser->getEmail()),
-        'name' => $this->currentUser->getAccountName(),
-      ]),
-      'registration' => $this->tincan->getLrsUuid($lms_id),
-    ];
-    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
-      $query_args['auth'] = 'Basic ' . \base64_encode($config['username'] . ':' . $config['password']);
+    $launch_url = $this->xapi->getLaunchUrl(
+      $item->getPackagePath(),
+      $item->getEntity(),
+      $this->currentUser,
+      $cacheable_metadata
+    );
+    if ($launch_url === NULL) {
+      return $no_cache_return;
     }
-    $launch_url .= '?' . \http_build_query($query_args);
 
     $build = [
       '#type' => 'html_tag',
diff --git a/src/TincanService.php b/src/XapiService.php
similarity index 58%
rename from src/TincanService.php
rename to src/XapiService.php
index 8c540fd37f6170e33bfac76eb834b045abbc7820..3f43e9d0f2e904895e99eaa810f8e00daf5f5a1d 100644
--- a/src/TincanService.php
+++ b/src/XapiService.php
@@ -4,11 +4,16 @@ declare(strict_types=1);
 
 namespace Drupal\lms_xapi;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\File\FileUrlGeneratorInterface;
+use Drupal\Core\Logger\LoggerChannelInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
 use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
 use TinCan\RemoteLRS;
@@ -18,7 +23,7 @@ use TinCan\Verb;
 /**
  * Tincan API integration utilities.
  */
-final class TincanService {
+final class XapiService {
 
   public const CONFIG_NAME = 'lms_xapi.settings';
 
@@ -28,6 +33,9 @@ final class TincanService {
     private \Closure $getConfig,
     #[AutowireIterator('xapi_id_generator')]
     private iterable $xapiIdGenerators,
+    private readonly FileUrlGeneratorInterface $fileUrlGenerator,
+    #[Autowire(service: 'logger.channel.lms_xapi', lazy: TRUE)]
+    private readonly LoggerChannelInterface $logger,
   ) {}
 
   /**
@@ -124,7 +132,7 @@ final class TincanService {
     if (!\str_starts_with($settings['endpoint'], 'http')) {
       $settings['endpoint'] = Url::fromRoute('<front>', [], [
         'absolute' => TRUE,
-      ])->toString() . $settings['endpoint'];
+      ])->toString() . \ltrim($settings['endpoint'], '/');
     }
 
     // @todo This logic is taken from opigno_tincan_activity module, should be
@@ -143,32 +151,38 @@ final class TincanService {
     $verb_passed = new Verb();
     $verb_passed->setId('http://adlnet.gov/expapi/verbs/passed');
 
-    $result = $lrs->queryStatements([
-      'registration' => $uuid,
-      'verb' => $verb_passed,
-      'limit' => 1,
-    ]);
-
-    $statements = [];
-    if (!empty($result->content) && is_object($result->content)) {
-      $statements = $result->content->getStatements();
-
-      // If nothing with "passed", test with "failed" verb.
-      if (count($statements) === 0) {
-        $verb_failed = new Verb();
-        $verb_failed->setId('http://adlnet.gov/expapi/verbs/failed');
-
-        $result = $lrs->queryStatements([
-          'registration' => $uuid,
-          'verb' => $verb_failed,
-          'limit' => 1,
-        ]);
-
-        if (!empty($result->content) && is_object($result->content)) {
-          $statements = $result->content->getStatements();
+    try {
+      $result = $lrs->queryStatements([
+        'registration' => $uuid,
+        'verb' => $verb_passed,
+        'limit' => 1,
+      ]);
+
+      $statements = [];
+      if (!empty($result->content) && is_object($result->content)) {
+        $statements = $result->content->getStatements();
+
+        // If nothing with "passed", test with "failed" verb.
+        if (count($statements) === 0) {
+          $verb_failed = new Verb();
+          $verb_failed->setId('http://adlnet.gov/expapi/verbs/failed');
+
+          $result = $lrs->queryStatements([
+            'registration' => $uuid,
+            'verb' => $verb_failed,
+            'limit' => 1,
+          ]);
+
+          if (!empty($result->content) && is_object($result->content)) {
+            $statements = $result->content->getStatements();
+          }
         }
       }
     }
+    catch (\Exception $e) {
+      $this->logger->error($e->getMessage());
+      return NULL;
+    }
 
     if (\count($statements) === 0) {
       return NULL;
@@ -207,4 +221,68 @@ final class TincanService {
     return NULL;
   }
 
+  /**
+   * Get package launch URL.
+   */
+  public function getLaunchUrl(
+    string $package_path,
+    EntityInterface $entity,
+    AccountInterface $account,
+    CacheableMetadata $cacheable_metadata,
+  ): ?string {
+    $tincan_file = $package_path . '/tincan.xml';
+    if (!\file_exists($tincan_file)) {
+      $this->logger->error(\sprintf('Xapi package file missing: %s.', $tincan_file));
+      return NULL;
+    }
+    $tincan_file_contents = \file_get_contents($tincan_file);
+    if ($tincan_file_contents === FALSE) {
+      $this->logger->error(\sprintf('Unable to get contents of %s file.', $tincan_file));
+      return NULL;
+    }
+
+    $config = ($this->getConfig)(self::CONFIG_NAME);
+    $launch_config = [
+      'endpoint' => $config->get('endpoint'),
+    ];
+    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
+      $launch_config += [
+        'username' => $config->get('username'),
+        'password' => $config->get('password'),
+      ];
+    }
+    foreach ($launch_config as $key => $value) {
+      if ($value === NULL || $value === '') {
+        $this->logger->error(\sprintf('Missing Xapi config: %s.', $key));
+        return NULL;
+      }
+    }
+
+    $xml = new \SimpleXMLElement($tincan_file_contents);
+    $launch_file = $package_path . '/' . $xml->activities->activity->launch;
+    $launch_url = $this->fileUrlGenerator->generateAbsoluteString($launch_file);
+
+    // Support relative URLs.
+    if (!\str_starts_with($launch_config['endpoint'], 'http')) {
+      $launch_config['endpoint'] = Url::fromRoute('<front>', [], [
+        'absolute' => TRUE,
+      ])->toString() . \ltrim($launch_config['endpoint'], '/');
+    }
+    $lms_id = $this->getLmsId($entity, $account, $cacheable_metadata);
+
+    $query_args = [
+      'endpoint' => $launch_config['endpoint'],
+      'actor' => Json::encode([
+        'mbox_sha1sum' => sha1('mailto:' . $account->getEmail()),
+        'name' => $account->getAccountName(),
+      ]),
+      'registration' => $this->getLrsUuid($lms_id),
+    ];
+    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
+      $query_args['auth'] = 'Basic ' . \base64_encode($launch_config['username'] . ':' . $launch_config['password']);
+    }
+    $launch_url .= '?' . \http_build_query($query_args);
+    return $launch_url;
+  }
+
 }