feat(badgeos): add BadgeOS award achievement action - #196
Conversation
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
- 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.
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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.
| bitsFetch({}, 'badgeos_authorize').then(result => { | ||
| if (result?.success) { | ||
| setIsAuthorized(true) | ||
| setSnackbar({ | ||
| show: true, | ||
| msg: __('Connected with BadgeOS Successfully', 'bit-integrations') | ||
| }) | ||
| } | ||
| setIsLoading(false) | ||
| setShowAuthMsg(true) | ||
| }) |
There was a problem hiding this comment.
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
- 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.
There was a problem hiding this comment.
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)
})| options={ | ||
| badgeOSConf?.allAchievements && | ||
| Array.isArray(badgeOSConf.allAchievements) && | ||
| badgeOSConf.allAchievements.map(achievement => ({ | ||
| label: `${achievement.achievement_name} (${achievement.achievement_type})`, | ||
| value: achievement?.achievement_id?.toString() | ||
| })) | ||
| } |
There was a problem hiding this comment.
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 [].
| 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() | |
| })) | |
| : [] | |
| } |
There was a problem hiding this comment.
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.
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
…n BadgeOS authorization
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 inbit-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_filterstriggerhandlers, so this is action-only — there is no trigger side to port.
Related Links: (if applicable)
Type of Change
Key Changes
Backend
BadgeOSControllerwith an activation check (class_exists('BadgeOS')),an authorize endpoint, and
refreshAchievements()which enumerates everyregistered achievement type via
badgeos_get_achievement_types_slugs()RecordApiHelperthat resolves the field map and dispatches to the Prohook, logging both success and failure through
LogHandler::saveRoutes.phpexposingbadgeos_authorizeandrefresh_badgeos_achievementsFrontend
BadgeOSintegration UI — authorization, field map, integrationlayout, create and edit wizards
NewInteg,EditInteg,IntegInfoandSelectAction(Pro-gated)Field mapping
user_idas a mapped field so the target user resolves from trigger dataconf.selectedAchievement), sinceit is a per-flow configuration choice rather than the identifier of the record
being acted on
Checklist
Changelog