Skip to content

feat(badgeos): add BadgeOS award achievement action - #196

Open
RishadAlam wants to merge 3 commits into
mainfrom
feat/badgeos
Open

feat(badgeos): add BadgeOS award achievement action#196
RishadAlam wants to merge 3 commits into
mainfrom
feat/badgeos

Conversation

@RishadAlam

Copy link
Copy Markdown
Member

Description

Adds BadgeOS as an action integration, ported from Bit-Pi. Provides a single
action — Award Achievement to User — which awards any BadgeOS achievement
(badge, rank, or custom achievement type) to a user when a flow runs.

Following the free/pro split, this plugin only validates input and fires
bit_integrations_badgeos_award_achievement_to_user; the award logic lives in
bit-integrations-pro.

Motivation & Context

BadgeOS exists as an integration in Bit-Pi but was missing from Bit Integrations
entirely. This closes that gap so gamification flows (award a badge on form
submit, on course completion, on purchase) can be built here.

Bit-Pi's BadgeOS integration ships no do_action/apply_filters trigger
handlers, so this is action-only — there is no trigger side to port.

Related Links: (if applicable)

Type of Change

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

Key Changes

Backend

  • Added BadgeOSController with an activation check (class_exists('BadgeOS')),
    an authorize endpoint, and refreshAchievements() which enumerates every
    registered achievement type via badgeos_get_achievement_types_slugs()
  • Added RecordApiHelper that resolves the field map and dispatches to the Pro
    hook, logging both success and failure through LogHandler::save
  • Added Routes.php exposing badgeos_authorize and refresh_badgeos_achievements

Frontend

  • Added the BadgeOS integration UI — authorization, field map, integration
    layout, create and edit wizards
  • Added the BadgeOS logo asset and registered the integration in NewInteg,
    EditInteg, IntegInfo and SelectAction (Pro-gated)

Field mapping

  • Added user_id as a mapped field so the target user resolves from trigger data
  • Added the achievement as a fetched dropdown (conf.selectedAchievement), since
    it is a per-flow configuration choice rather than the identifier of the record
    being acted on

Checklist

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

Changelog

  • New Actions
  • BadgeOS: Award Achievement to User (Pro).

Ports the BadgeOS "Award Achievement to User" action from Bit-Pi.

The free side only fires bit_integrations_badgeos_award_achievement_to_user;
all award logic lives in the Pro plugin. bit-pi exposes no wp_hook triggers
for BadgeOS, so this is action-only.

user_id is field-mapped so it can be resolved from trigger data, while the
achievement is a fetched dropdown (conf.selectedAchievement) since it is a
per-flow config choice rather than the identifier of the record being acted
on.
Copilot AI review requested due to automatic review settings July 18, 2026 11:55

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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 introduces a new integration for BadgeOS, adding backend controllers, API helpers, and routes, alongside frontend React components for authorization, field mapping, and configuration. The review feedback suggests declaring the refreshAchievements controller method as static, correcting a typo in a backend error message, adding a .catch block to handle API request failures in the authorization component, and safely defaulting to an empty array when rendering achievements to prevent potential UI rendering issues.

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.

wp_send_json_success(true);
}

public function refreshAchievements()

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

The refreshAchievements method does not access any instance properties ($this) and is registered as a static route callback in Routes.php. To maintain consistency and adhere to the project's coding standards, it should be explicitly declared as static.

    public static function refreshAchievements()
References
  1. In PHP, controller methods that do not access instance state ($this) and are registered as static route callbacks should be explicitly declared as static to maintain consistency and adhere to coding standards.

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.

Not applying this one — it reads as a style preference rather than a defect here.

Route::action() dispatches through reflection and explicitly supports both forms:

$response = $reflectionMethod->invoke($reflectionMethod->isStatic() ? null : new $invokeable[0](), $data);

So a non-static callback gets instantiated and invoked correctly. There are 30 non-static refresh* methods across backend/Actions/ today, and this controller was cloned from FluentCartController, whose refresh methods are also non-static. Keeping it consistent with the file it was modelled on.

$defaultResponse = [
'success' => false,
// translators: %s: Plugin name
'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro')

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

There is a typo in the user-facing error message: 'activate' should be 'activated'.

            'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro')

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.

Good catch — fixed:

'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro')

Worth flagging for a follow-up: the misspelled string is present in 18 other files under backend/Actions/, since it gets copied along with each new integration. Correcting it only here forks the translatable string, so a repo-wide sweep in its own commit would be the cleaner fix.

Comment on lines +23 to +33
bitsFetch({}, 'badgeos_authorize').then(result => {
if (result?.success) {
setIsAuthorized(true)
setSnackbar({
show: true,
msg: __('Connected with BadgeOS Successfully', 'bit-integrations')
})
}
setIsLoading(false)
setShowAuthMsg(true)
})

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

When handling asynchronous API requests, always append a .catch block to ensure that loading states are reset (e.g., setting isLoading to false) if the request fails or encounters a network error.

    bitsFetch({}, 'badgeos_authorize')
      .then(result => {
        if (result?.success) {
          setIsAuthorized(true)
          setSnackbar({
            show: true,
            msg: __('Connected with BadgeOS Successfully', 'bit-integrations')
          })
        }
        setIsLoading(false)
        setShowAuthMsg(true)
      })
      .catch(() => setIsLoading(false))
References
  1. When handling asynchronous API requests, always append a .catch block to ensure that loading states are reset and error messages are displayed if the request fails or encounters a network error.

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.

Valid — this was a real defect, fixed.

Without a .catch, a rejected request left isLoading stuck at 'auth', which permanently disabled both the Connect and Next buttons with no way to recover short of a reload. The catch also sets showAuthMsg so the failure surfaces in the UI rather than failing silently:

bitsFetch({}, 'badgeos_authorize')
  .then(result => {
    if (result?.success) {
      setIsAuthorized(true)
      setSnackbar({
        show: true,
        msg: __('Connected with BadgeOS Successfully', 'bit-integrations')
      })
    }
    setIsLoading(false)
    setShowAuthMsg(true)
  })
  .catch(() => {
    setIsLoading(false)
    setShowAuthMsg(true)
  })

Comment on lines +76 to +83
options={
badgeOSConf?.allAchievements &&
Array.isArray(badgeOSConf.allAchievements) &&
badgeOSConf.allAchievements.map(achievement => ({
label: `${achievement.achievement_name} (${achievement.achievement_type})`,
value: achievement?.achievement_id?.toString()
}))
}

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

If badgeOSConf.allAchievements is not defined or not an array, the expression currently evaluates to undefined or false. Passing false to the options prop of MultiSelect can cause component rendering issues. It is safer to use a ternary operator to default to an empty array [].

Suggested change
options={
badgeOSConf?.allAchievements &&
Array.isArray(badgeOSConf.allAchievements) &&
badgeOSConf.allAchievements.map(achievement => ({
label: `${achievement.achievement_name} (${achievement.achievement_type})`,
value: achievement?.achievement_id?.toString()
}))
}
options={
Array.isArray(badgeOSConf?.allAchievements)
? badgeOSConf.allAchievements.map(achievement => ({
label: achievement.achievement_name + ' (' + achievement.achievement_type + ')',
value: achievement?.achievement_id?.toString()
}))
: []
}

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.

Agreed — fixed. The && chain evaluated to undefined before the first fetch and false for a non-array, neither of which is a valid options value:

options={
  Array.isArray(badgeOSConf?.allAchievements)
    ? badgeOSConf.allAchievements.map(achievement => ({
        label: `${achievement.achievement_name} (${achievement.achievement_type})`,
        value: achievement?.achievement_id?.toString()
      }))
    : []
}

The Array.isArray check already covers null/undefined, so the separate truthiness test was redundant and has been dropped.

@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


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

Copilot AI review requested due to automatic review settings July 18, 2026 12:02

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants