Skip to content

feat(masterstudy-lms): add user enroll/unenroll & course/lesson completion actions#182

Open
RishadAlam wants to merge 4 commits into
mainfrom
feat/masterstudy
Open

feat(masterstudy-lms): add user enroll/unenroll & course/lesson completion actions#182
RishadAlam wants to merge 4 commits into
mainfrom
feat/masterstudy

Conversation

@RishadAlam

Copy link
Copy Markdown
Member

Description

Adds four new MasterStudy LMS actions — Enroll user in a course, Unenroll user from a course, Mark a course complete for the user, and Mark a lesson complete for the user — targeting any user by email. The existing logged-in-user actions (complete/reset course, lesson, quiz) are untouched.

Motivation & Context

The integration previously only acted on the currently logged-in user. These actions let a flow operate on a specific user (resolved from a mapped email field), enabling automations like "when X happens, enroll this user / mark their course complete". The actual write logic lives in the Pro plugin; the free plugin fires the bit_integrations_master_study_lms_* hooks.

Related Links: (if applicable)

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 📚 Documentation update
  • ⚡ Improvement
  • 🔄 Code refactor

Key Changes

Integrations — Actions

  • Added four email-targeted actions (keys 6–9): Enroll, Unenroll, Mark Course Complete, Mark Lesson Complete. Each fires a bit_integrations_master_study_lms_* filter handled by the Pro plugin; the free side validates and logs the result.
  • Added a user_email field-map row for the new actions so the target user can be mapped dynamically from a trigger field (form field, custom value, or smart code) — not a static text input.

Frontend

  • Updated the action picker to a pro-gated MultiSelect (checkIsPro/getProLabel) so the new Pro actions show a (Pro) badge and are disabled for non-Pro users.
  • Added MasterStudyLmsFieldMap (clone of the standard field-map row) and MasterStudyLmsCommonFunc helpers (MS_LMS_ACTIONS constants, allActions, generateMappedField, isUserEmailMapped, isActionConfigIncomplete).
  • Fixed a pre-existing ReferenceError on the course-refresh button (fetchAllCoursefetchAllMsLmsCourse).
  • Fixed Edit screen not validating required fields — save is now gated by the same isActionConfigIncomplete check as the New wizard.
  • Fixed loss of a mapped user email when switching between two email actions.
  • Refactored magic action numbers to named constants; moved allActions into the common helper to remove a circular import; removed a dead handleInput prop.

Backend

  • Added branches for the new actions in RecordApiHelper that build field-map data and dispatch to the Pro hooks, with null-safe response handling and course/lesson id guards.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Tests added/updated
  • Documentation updated if needed
  • README updated if needed

Changelog

  • New Actions: MasterStudy LMS — Enroll user in a course, Unenroll user from a course, Mark a course complete for the user, Mark a lesson complete for the user
  • Improvement: Target user for the new MasterStudy LMS actions is selected via field mapping (supports dynamic values), and Pro-only actions are clearly marked
  • Fix: MasterStudy LMS course-refresh button no longer throws a console error

- guard non-array hook response and null course/lesson ids
- share per-action validation (isActionConfigIncomplete) with edit screen
- preserve mapped user email when switching between email actions
- move allActions to common helper (break circular import)
- drop dead handleInput prop from integ layout

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the MasterStudy LMS integration by introducing constants for actions, implementing new Pro actions (Enroll User, Unenroll User, Mark Course Complete, and Mark Lesson Complete), and adding field mapping support for user emails. The frontend is updated to dynamically validate action configurations and render the appropriate mapping fields. The review feedback suggests using is_wp_error() in the backend to safely handle potential WordPress error objects before accessing them as arrays, preventing fatal errors in PHP 8.0+. Additionally, it recommends using falsy checks instead of strict undefined checks in the frontend configuration validation to more robustly identify incomplete fields.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

'lesson_id' => $integrationDetails->lessonId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

Comment on lines +68 to +87
export const isActionConfigIncomplete = conf => {
switch (conf?.mainAction) {
case MS_LMS_ACTIONS.COMPLETE_COURSE:
case MS_LMS_ACTIONS.RESET_COURSE:
return conf.courseId === undefined
case MS_LMS_ACTIONS.COMPLETE_LESSON:
case MS_LMS_ACTIONS.RESET_LESSON:
return conf.lessonId === undefined
case MS_LMS_ACTIONS.COMPLETE_QUIZ:
return conf.quizId === undefined
case MS_LMS_ACTIONS.ENROLL_USER:
case MS_LMS_ACTIONS.UNENROLL_USER:
case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE:
return conf.courseId === undefined || !isUserEmailMapped(conf)
case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE:
return conf.courseId === undefined || conf.lessonId === undefined || !isUserEmailMapped(conf)
default:
return false
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using strict equality checks against undefined can fail if the values are null or empty strings. It is safer and more robust to use falsy checks (e.g., !conf.courseId) to ensure that missing, null, or empty values are correctly identified as incomplete.

Suggested change
export const isActionConfigIncomplete = conf => {
switch (conf?.mainAction) {
case MS_LMS_ACTIONS.COMPLETE_COURSE:
case MS_LMS_ACTIONS.RESET_COURSE:
return conf.courseId === undefined
case MS_LMS_ACTIONS.COMPLETE_LESSON:
case MS_LMS_ACTIONS.RESET_LESSON:
return conf.lessonId === undefined
case MS_LMS_ACTIONS.COMPLETE_QUIZ:
return conf.quizId === undefined
case MS_LMS_ACTIONS.ENROLL_USER:
case MS_LMS_ACTIONS.UNENROLL_USER:
case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE:
return conf.courseId === undefined || !isUserEmailMapped(conf)
case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE:
return conf.courseId === undefined || conf.lessonId === undefined || !isUserEmailMapped(conf)
default:
return false
}
}
export const isActionConfigIncomplete = conf => {
switch (conf?.mainAction) {
case MS_LMS_ACTIONS.COMPLETE_COURSE:
case MS_LMS_ACTIONS.RESET_COURSE:
return !conf.courseId
case MS_LMS_ACTIONS.COMPLETE_LESSON:
case MS_LMS_ACTIONS.RESET_LESSON:
return !conf.lessonId
case MS_LMS_ACTIONS.COMPLETE_QUIZ:
return !conf.quizId
case MS_LMS_ACTIONS.ENROLL_USER:
case MS_LMS_ACTIONS.UNENROLL_USER:
case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE:
return !conf.courseId || !isUserEmailMapped(conf)
case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE:
return !conf.courseId || !conf.lessonId || !isUserEmailMapped(conf)
default:
return false
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied — switched to falsy checks so null/empty values are also treated as incomplete:

case MS_LMS_ACTIONS.COMPLETE_COURSE:
case MS_LMS_ACTIONS.RESET_COURSE:
  return !conf.courseId
...
case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE:
  return !conf.courseId || !conf.lessonId || !isUserEmailMapped(conf)

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
1 0 1

⚠️ Warnings (1)

📁 readme.txt (1 warning)
📍 Line 🔖 Check 💬 Message
0 mismatched_plugin_name Plugin name "Bit integrations - Form Integration, Webhook, Spreadsheets, CRM, LMS & Email Automation" is different from the name declared in plugin header "Bit Integrations".

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant