Claude Skill

lw-lms-rest-frontend

Build a custom frontend against LW LMS's headless `/wp-json/lms/v1` REST API in lw-lms v1.6.0. Use for React, Vue, Astro, mobile, or theme code that calls `/courses`, `/courses/{id}`, `/lessons/{id}`, `/progress`, `/progress/course/{id}`, or `/download/{id}` and must handle publi

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-lw-plugins_lw-lms-rest-frontend-52f6020.zip · 6 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/lw-plugins/lw-lms-rest-frontend
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

LW LMS: REST frontend consumer

For frontend developers consuming LW LMS data: course catalog, course detail, lesson player, progress dashboard, and protected downloads. The core lw-lms plugin ships a headless REST API; it does not ship public templates, shortcodes, or blocks.

BETA NOTICE. The plugin README says the plugin is under active development and not recommended for production use. Snapshot the JSON shapes in tests and review CHANGELOG.md before upgrading. This skill is verified against local lw-lms v1.6.0.

Version deltas that matter

  • v1.6.0: no route or JSON-schema change. A server-side lw_lms_has_course_access callback can now alter the final logged-in paid-course access decision after built-in access checks, which affects access.has_access, lesson accessibility, attachments, lesson detail, progress writes, and protected downloads.
  • v1.5.1: maintenance release, no functional REST changes.
  • v1.5.0: paid-course denied access payload can include memberships when WooCommerce Memberships is active and the course has _lw_lms_membership_plan_ids.
  • v1.4.0: course content in /courses/{id} is public marketing/about content. It is no longer gated behind access.has_access. Lesson content is still gated by /lessons/{id}.
  • v1.4.0: open-course lessons remain accessible to guests even if marked as preview.
  • v1.3.0: first logged-in access to a free course lazily writes a source='free' access row server-side; the REST consumer does not need to do anything special.
  • v1.2.15: denied paid-course access payload can include subscription_variations.

Misconception this skill corrects

"I should use course.content as the access gate."

Wrong for v1.4.0+. CourseTransformer::transform_full() always includes the course content; this is the public course description. Use course.access.has_access and each lesson's accessible flag for gating.

// WRONG: course content is public and is not proof of access.
if (course.content) {
    renderLessonPlayer();
}

// RIGHT: access comes from access.has_access and per-lesson accessible.
if (course.access.has_access) {
    return <CourseContent html={course.content} progress={course.progress} />;
}

return <PurchaseGate access={course.access} />;

Lesson body content still comes from GET /lms/v1/lessons/{id} and returns 403 forbidden when AccessChecker::has_lesson_access() denies the request.

In v1.6.0, a companion plugin may provide the final logged-in paid-course decision through lw_lms_has_course_access. Frontend code should continue to trust the server response; it must not reproduce membership or entitlement rules in JavaScript. Backend callbacks must be deterministic: the full-course transformer checks course access twice, so a changing result can make access.has_access disagree with lesson/attachment gating in one response.

REST API surface

Namespace: lms/v1, mounted at /wp-json/lms/v1/....

Method Path Auth Purpose
GET /lms/v1/courses public Paginated public course list
GET /lms/v1/courses/{id} public Single course detail; course content is public, lesson rows carry accessible
GET /lms/v1/lessons/{id} public route, access-gated body Single lesson; 403 without lesson access
GET /lms/v1/progress logged-in Current user's all progress rows
GET /lms/v1/progress/course/{id} logged-in Current user's progress rows for one course plus course summary progress
POST /lms/v1/progress logged-in + lesson access Upsert lesson status
GET /lms/v1/download/{id} public route, access-gated file Stream protected attachment binary

Detailed response contract

Read references/rest-response-contract.md when implementing request parameters, full JSON shapes, nullable fields, lesson errors, progress validation, or binary downloads. The route/auth/access decisions and client safety rules remain in this main skill.

Authentication

Context Auth pattern
Public catalog and course detail no auth
Logged-in browser UI WordPress cookie + X-WP-Nonce, usually via wp.apiFetch
Headless/mobile Application Password Basic auth, or a vetted JWT/OAuth layer
Downloads same auth context as the protected course/lesson

For browser fetch, include both nonce and credentials:

await fetch('/wp-json/lms/v1/progress', {
  method: 'POST',
  credentials: 'same-origin',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': wpApiSettings.nonce
  },
  body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })
});

Critical rules

  • Course content is public in v1.4.0+; never use it as the access gate.
  • A server-side v1.6.0 paid-access filter may change access without creating an enrollment row; the REST response remains the authority for the client.
  • Lesson detail and download endpoints are the protected surfaces.
  • Iterate both sections and lessons_without_section.
  • Render locked lessons disabled, not hidden.
  • access.memberships is available only when WooCommerce Memberships functions exist and linked plans are configured.
  • List endpoint access info is intentionally small; build purchase gates from the single-course response.
  • course_progress uses completed_lessons, not completed_count.
  • completed_at is null for non-completed rows.
  • content_raw is editor-only source content; do not render it in public UI.
  • download_url is already a full REST URL; do not reconstruct it by concatenating /wp-json.
  • The download endpoint returns binary data; do not call response.json() on it.

Common mistakes

// WRONG: only iterating sections.
course.sections.map(section => <Section section={section} />);

// RIGHT: include orphan lessons too.
<>
  {course.sections.map(section => <Section key={section.id} section={section} />)}
  {course.lessons_without_section.length > 0 && (
    <LessonGroup lessons={course.lessons_without_section} />
  )}
</>
// WRONG: old pre-1.4 assumption.
const canAccess = Boolean(course.content);

// RIGHT.
const canAccess = course.access.has_access;
// WRONG: progress key from stale docs.
<ProgressBar value={data.course_progress.completed_count} />

// RIGHT.
<ProgressBar value={data.course_progress.completed_lessons} />
// WRONG: POST body missing course_id.
fetch('/wp-json/lms/v1/progress', {
  method: 'POST',
  body: JSON.stringify({ lesson_id: 100, status: 'completed' })
});

// RIGHT.
fetch('/wp-json/lms/v1/progress', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce },
  credentials: 'same-origin',
  body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })
});

Cross-references

  • Use lw-lms-backend-extend for hooks, access/progress repositories, and companion-plugin backend integration.
  • Use lw-lms-abilities for admin/agent lw-lms/* Abilities API calls; those are not the learner-facing REST endpoints.
  • Use lw-lms-wp-cli-operations for operational WP-CLI course, lesson, enrollment, revoke, and force-complete commands.
  • Use lw-lms-learndash-migration for the one-time LearnDash migration command.

What this skill does NOT cover

  • A separate frontend plugin. This skill documents the core lw-lms REST contract only.
  • Payment processing. Link to WooCommerce products/subscriptions/membership join URLs and let WooCommerce handle checkout.
  • Custom REST route registration. Use normal WordPress register_rest_route() patterns in a companion plugin.
  • Server-rendered theme templates. You can consume REST server-side, but direct CPT/meta queries are usually simpler in PHP templates.

References

  • REST namespace and route registration: includes/Api/RestApi.php.
  • Course list/detail routes: includes/Api/Controllers/CoursesController.php.
  • Lesson route and 403 behavior: includes/Api/Controllers/LessonsController.php.
  • Progress routes and cross-validation: includes/Api/Controllers/ProgressController.php.
  • Download route and binary response: includes/Api/Controllers/DownloadController.php.
  • Course response shape and public content: includes/Api/Transformers/CourseTransformer.php.
  • Lesson response shape: includes/Api/Transformers/LessonTransformer.php.
  • Progress row shape: includes/Api/Transformers/ProgressTransformer.php.
  • Paid access payload including memberships: includes/Access/AccessChecker.php, includes/Access/MembershipChecker.php.
  • Official documentation: https://github.com/lwplugins/lw-lms
  • Verified source paths:
    • wp-content/plugins/lw-lms/includes/Access/SubscriptionVariationChecker.php
    • wp-content/plugins/lw-lms/includes/Access/WooCommerceChecker.php
    • wp-content/plugins/lw-lms/includes/Meta/VideoParser.php
    • wp-content/plugins/lw-lms/includes/Progress/ProgressCalculator.php
    • wp-content/plugins/lw-lms/CHANGELOG.md
Files (wp-agent-skills)
  • references
    • rest-response-contract.md 6.1 KB
      # LW LMS v1.6.0 REST response contract
      
      Load this reference when implementing the learner-facing response parser or request builder. There was no route or schema change in v1.6.0; its paid-course filter changes only the server-calculated access decision.
      
      ## Course list
      
      ```http
      GET /wp-json/lms/v1/courses?per_page=12&page=1&category=php&level=beginner&search=oop
      ```
      
      | Param | Default | Validation |
      |---|---|---|
      | `per_page` | `10` | integer `1..100` |
      | `page` | `1` | integer `>= 1` |
      | `category` | `''` | course category slug |
      | `level` | `''` | course level slug |
      | `search` | `''` | text search |
      
      ```json
      {
        "data": [
          {
            "id": 42,
            "title": "PHP Object-Oriented Fundamentals",
            "slug": "php-oop-fundamentals",
            "excerpt": "Short summary.",
            "thumbnail": "https://site.test/wp-content/uploads/course.jpg",
            "categories": [{ "id": 5, "name": "PHP", "slug": "php" }],
            "level": { "id": 9, "name": "Beginner", "slug": "beginner" },
            "duration": "8h",
            "lesson_count": 24,
            "access": { "type": "paid", "has_access": false }
          }
        ],
        "meta": { "total": 47, "pages": 4, "current_page": 1, "per_page": 12 }
      }
      ```
      
      List items omit products, subscriptions, subscription variations, memberships, expiry, sections, attachments, and progress. Fetch the single course for those.
      
      ## Course detail
      
      ```http
      GET /wp-json/lms/v1/courses/42
      ```
      
      | Field | Presence |
      |---|---|
      | `content` | Always present for published courses; public marketing/about description |
      | `content_raw` | Editors only (`current_user_can( 'edit_posts' )`) |
      | `sections` | Always present; each section contains lesson list rows |
      | `lessons_without_section` | Always present; render after sections |
      | `attachments` | Populated only when `access.has_access === true`, otherwise empty |
      | `progress` | Present for logged-in users |
      | `access.expires_at` | Paid course with an access row and non-empty expiry |
      | `access.products` | Denied paid course with linked WooCommerce products |
      | `access.subscriptions` | Denied paid course with linked parent subscriptions |
      | `access.subscription_variations` | Denied paid course with linked variation pairs |
      | `access.memberships` | Denied paid course with linked WooCommerce Memberships plans |
      
      ```json
      {
        "id": 42,
        "title": "PHP Object-Oriented Fundamentals",
        "content": "<p>Public course description.</p>",
        "access": {
          "type": "paid",
          "has_access": false,
          "requires": "purchase",
          "products": [
            { "id": 200, "name": "PHP OOP Course", "price": "49.00", "price_formatted": "$49.00", "url": "...", "access_duration": 0 }
          ],
          "subscriptions": [
            { "id": 201, "name": "All Access", "price": "19.00", "price_formatted": "$19.00 / month", "url": "..." }
          ],
          "subscription_variations": [
            { "parent_id": 300, "variation_id": 305, "name": "All Access - Yearly", "attributes": {"attribute_plan": "yearly"}, "price": "190.00", "price_formatted": "$190.00 / year", "url": "..." }
          ],
          "memberships": [
            { "id": 77, "name": "Pro Members", "join": "https://site.test/product/pro-membership/" }
          ]
        },
        "sections": [
          {
            "id": "sec_intro",
            "title": "Getting Started",
            "description": "",
            "order": 0,
            "lessons": [
              { "id": 100, "title": "What is OOP", "order": 0, "duration": "12 min", "preview": true, "accessible": true, "completed": false }
            ]
          }
        ],
        "lessons_without_section": [],
        "attachments": [],
        "progress": { "completed_lessons": 0, "total_lessons": 24, "percentage": 0 }
      }
      ```
      
      Render both `sections[].lessons` and `lessons_without_section`. Show inaccessible lessons as locked/disabled instead of hiding the syllabus.
      
      ## Lesson detail
      
      ```http
      GET /wp-json/lms/v1/lessons/100
      ```
      
      - Open-course lessons are available to guests.
      - Free/paid course lessons require course access unless marked preview.
      - Preview lessons require a logged-in user in the current source.
      - Denial returns `403 forbidden`; missing/non-published lessons return `404 not_found`.
      
      ```json
      {
        "id": 100,
        "title": "What is OOP",
        "content": "<p>Lesson body.</p>",
        "course": { "id": 42, "title": "PHP OOP Fundamentals" },
        "section": { "id": "sec_intro", "title": "Getting Started" },
        "order": 0,
        "duration": "12 min",
        "video": { "url": "https://www.youtube.com/watch?v=abc123", "provider": "youtube", "video_id": "abc123", "embed": "https://www.youtube.com/embed/abc123", "duration": "" },
        "attachments": [
          { "id": 250, "title": "Slides.pdf", "filename": "slides.pdf", "mime_type": "application/pdf", "size": 245678, "download_url": "https://site.test/wp-json/lms/v1/download/250" }
        ],
        "navigation": { "previous": null, "next": { "id": 101, "title": "Next lesson" } }
      }
      ```
      
      `video`, `section`, `navigation.previous`, and `navigation.next` can be null. Switch video rendering on `video.provider`, not URL matching.
      
      ## Progress
      
      ```http
      GET /wp-json/lms/v1/progress
      GET /wp-json/lms/v1/progress/course/42
      ```
      
      The course-scoped response adds the authoritative `course_progress` summary:
      
      ```json
      {
        "data": [
          {
            "user_id": 5,
            "course_id": 42,
            "lesson_id": 100,
            "status": "completed",
            "completed_at": "2026-04-15 10:30:00",
            "created_at": "2026-04-14 09:00:00",
            "updated_at": "2026-04-15 10:30:00"
          }
        ],
        "course_progress": { "completed_lessons": 5, "total_lessons": 24, "percentage": 21 }
      }
      ```
      
      ```http
      POST /wp-json/lms/v1/progress
      Content-Type: application/json
      
      {
        "course_id": 42,
        "lesson_id": 100,
        "status": "completed"
      }
      ```
      
      `course_id`, `lesson_id`, and `status` are required. Status is `not_started`, `in_progress`, or `completed`. The server returns `401 unauthorized` for a guest, `403 forbidden` without lesson access, `400 invalid_request` when the lesson does not belong to the course, and `500 update_failed` on write failure. Success returns the saved row and authoritative `course_progress`.
      
      ## Downloads
      
      `download_url` points at `/lms/v1/download/{attachment_id}` and returns a binary stream, not JSON. Use a normal link or fetch it as a blob. The controller scans `_lw_lms_attachments` on course/lesson posts, applies course/lesson access, fires `lw_lms_attachment_downloaded` on success, and sends file headers.
      
  • SKILL.md 9.6 KB
    ---
    name: lw-lms-rest-frontend
    description: Build a custom frontend against LW LMS's headless `/wp-json/lms/v1` REST API in lw-lms v1.6.0. Use for React, Vue, Astro, mobile, or theme code that calls `/courses`, `/courses/{id}`, `/lessons/{id}`, `/progress`, `/progress/course/{id}`, or `/download/{id}` and must handle public course content, lesson/access gating, paid-course products/subscriptions/subscription_variations/memberships, custom paid-access decisions, progress payloads, downloads, and nonce/app-password auth.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "lw-lms"
      wp-skills-plugin-version-tested: "1.6.0"
      wp-skills-php-min: "8.2"
      wp-skills-last-updated: "2026-07-20"
    ---
    
    # LW LMS: REST frontend consumer
    
    For frontend developers consuming LW LMS data: course catalog, course detail, lesson player, progress dashboard, and protected downloads. The core `lw-lms` plugin ships a headless REST API; it does not ship public templates, shortcodes, or blocks.
    
    > **BETA NOTICE.** The plugin README says the plugin is under active development and not recommended for production use. Snapshot the JSON shapes in tests and review `CHANGELOG.md` before upgrading. This skill is verified against local lw-lms **v1.6.0**.
    
    ## Version deltas that matter
    
    - **v1.6.0**: no route or JSON-schema change. A server-side `lw_lms_has_course_access` callback can now alter the final logged-in paid-course access decision after built-in access checks, which affects `access.has_access`, lesson accessibility, attachments, lesson detail, progress writes, and protected downloads.
    - **v1.5.1**: maintenance release, no functional REST changes.
    - **v1.5.0**: paid-course denied `access` payload can include `memberships` when WooCommerce Memberships is active and the course has `_lw_lms_membership_plan_ids`.
    - **v1.4.0**: course `content` in `/courses/{id}` is public marketing/about content. It is no longer gated behind `access.has_access`. Lesson content is still gated by `/lessons/{id}`.
    - **v1.4.0**: open-course lessons remain accessible to guests even if marked as preview.
    - **v1.3.0**: first logged-in access to a free course lazily writes a `source='free'` access row server-side; the REST consumer does not need to do anything special.
    - **v1.2.15**: denied paid-course `access` payload can include `subscription_variations`.
    
    ## Misconception this skill corrects
    
    > "I should use `course.content` as the access gate."
    
    Wrong for v1.4.0+. `CourseTransformer::transform_full()` always includes the course `content`; this is the public course description. Use `course.access.has_access` and each lesson's `accessible` flag for gating.
    
    ```jsx
    // WRONG: course content is public and is not proof of access.
    if (course.content) {
        renderLessonPlayer();
    }
    
    // RIGHT: access comes from access.has_access and per-lesson accessible.
    if (course.access.has_access) {
        return <CourseContent html={course.content} progress={course.progress} />;
    }
    
    return <PurchaseGate access={course.access} />;
    ```
    
    Lesson body content still comes from `GET /lms/v1/lessons/{id}` and returns `403 forbidden` when `AccessChecker::has_lesson_access()` denies the request.
    
    In v1.6.0, a companion plugin may provide the final logged-in paid-course decision through `lw_lms_has_course_access`. Frontend code should continue to trust the server response; it must not reproduce membership or entitlement rules in JavaScript. Backend callbacks must be deterministic: the full-course transformer checks course access twice, so a changing result can make `access.has_access` disagree with lesson/attachment gating in one response.
    
    ## REST API surface
    
    Namespace: `lms/v1`, mounted at `/wp-json/lms/v1/...`.
    
    | Method | Path | Auth | Purpose |
    |---|---|---|---|
    | `GET` | `/lms/v1/courses` | public | Paginated public course list |
    | `GET` | `/lms/v1/courses/{id}` | public | Single course detail; course content is public, lesson rows carry `accessible` |
    | `GET` | `/lms/v1/lessons/{id}` | public route, access-gated body | Single lesson; 403 without lesson access |
    | `GET` | `/lms/v1/progress` | logged-in | Current user's all progress rows |
    | `GET` | `/lms/v1/progress/course/{id}` | logged-in | Current user's progress rows for one course plus course summary progress |
    | `POST` | `/lms/v1/progress` | logged-in + lesson access | Upsert lesson status |
    | `GET` | `/lms/v1/download/{id}` | public route, access-gated file | Stream protected attachment binary |
    
    ## Detailed response contract
    
    Read [references/rest-response-contract.md](references/rest-response-contract.md) when implementing request parameters, full JSON shapes, nullable fields, lesson errors, progress validation, or binary downloads. The route/auth/access decisions and client safety rules remain in this main skill.
    
    ## Authentication
    
    | Context | Auth pattern |
    |---|---|
    | Public catalog and course detail | no auth |
    | Logged-in browser UI | WordPress cookie + `X-WP-Nonce`, usually via `wp.apiFetch` |
    | Headless/mobile | Application Password Basic auth, or a vetted JWT/OAuth layer |
    | Downloads | same auth context as the protected course/lesson |
    
    For browser `fetch`, include both nonce and credentials:
    
    ```js
    await fetch('/wp-json/lms/v1/progress', {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': wpApiSettings.nonce
      },
      body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })
    });
    ```
    
    ## Critical rules
    
    - Course `content` is public in v1.4.0+; never use it as the access gate.
    - A server-side v1.6.0 paid-access filter may change access without creating an enrollment row; the REST response remains the authority for the client.
    - Lesson detail and download endpoints are the protected surfaces.
    - Iterate both `sections` and `lessons_without_section`.
    - Render locked lessons disabled, not hidden.
    - `access.memberships` is available only when WooCommerce Memberships functions exist and linked plans are configured.
    - List endpoint access info is intentionally small; build purchase gates from the single-course response.
    - `course_progress` uses `completed_lessons`, not `completed_count`.
    - `completed_at` is `null` for non-completed rows.
    - `content_raw` is editor-only source content; do not render it in public UI.
    - `download_url` is already a full REST URL; do not reconstruct it by concatenating `/wp-json`.
    - The download endpoint returns binary data; do not call `response.json()` on it.
    
    ## Common mistakes
    
    ```jsx
    // WRONG: only iterating sections.
    course.sections.map(section => <Section section={section} />);
    
    // RIGHT: include orphan lessons too.
    <>
      {course.sections.map(section => <Section key={section.id} section={section} />)}
      {course.lessons_without_section.length > 0 && (
        <LessonGroup lessons={course.lessons_without_section} />
      )}
    </>
    ```
    
    ```jsx
    // WRONG: old pre-1.4 assumption.
    const canAccess = Boolean(course.content);
    
    // RIGHT.
    const canAccess = course.access.has_access;
    ```
    
    ```jsx
    // WRONG: progress key from stale docs.
    <ProgressBar value={data.course_progress.completed_count} />
    
    // RIGHT.
    <ProgressBar value={data.course_progress.completed_lessons} />
    ```
    
    ```js
    // WRONG: POST body missing course_id.
    fetch('/wp-json/lms/v1/progress', {
      method: 'POST',
      body: JSON.stringify({ lesson_id: 100, status: 'completed' })
    });
    
    // RIGHT.
    fetch('/wp-json/lms/v1/progress', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce },
      credentials: 'same-origin',
      body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })
    });
    ```
    
    ## Cross-references
    
    - Use `lw-lms-backend-extend` for hooks, access/progress repositories, and companion-plugin backend integration.
    - Use `lw-lms-abilities` for admin/agent `lw-lms/*` Abilities API calls; those are not the learner-facing REST endpoints.
    - Use `lw-lms-wp-cli-operations` for operational WP-CLI course, lesson, enrollment, revoke, and force-complete commands.
    - Use `lw-lms-learndash-migration` for the one-time LearnDash migration command.
    
    ## What this skill does NOT cover
    
    - A separate frontend plugin. This skill documents the core `lw-lms` REST contract only.
    - Payment processing. Link to WooCommerce products/subscriptions/membership join URLs and let WooCommerce handle checkout.
    - Custom REST route registration. Use normal WordPress `register_rest_route()` patterns in a companion plugin.
    - Server-rendered theme templates. You can consume REST server-side, but direct CPT/meta queries are usually simpler in PHP templates.
    
    ## References
    
    - REST namespace and route registration: `includes/Api/RestApi.php`.
    - Course list/detail routes: `includes/Api/Controllers/CoursesController.php`.
    - Lesson route and 403 behavior: `includes/Api/Controllers/LessonsController.php`.
    - Progress routes and cross-validation: `includes/Api/Controllers/ProgressController.php`.
    - Download route and binary response: `includes/Api/Controllers/DownloadController.php`.
    - Course response shape and public `content`: `includes/Api/Transformers/CourseTransformer.php`.
    - Lesson response shape: `includes/Api/Transformers/LessonTransformer.php`.
    - Progress row shape: `includes/Api/Transformers/ProgressTransformer.php`.
    - Paid access payload including memberships: `includes/Access/AccessChecker.php`, `includes/Access/MembershipChecker.php`.
    - Official documentation: <https://github.com/lwplugins/lw-lms>
    - Verified source paths:
      - `wp-content/plugins/lw-lms/includes/Access/SubscriptionVariationChecker.php`
      - `wp-content/plugins/lw-lms/includes/Access/WooCommerceChecker.php`
      - `wp-content/plugins/lw-lms/includes/Meta/VideoParser.php`
      - `wp-content/plugins/lw-lms/includes/Progress/ProgressCalculator.php`
      - `wp-content/plugins/lw-lms/CHANGELOG.md`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related