feat(profilepress): add ProfilePress membership integration#198
feat(profilepress): add ProfilePress membership integration#198RishadAlam wants to merge 5 commits into
Conversation
Ports the ProfilePress write actions from Bit-Pi: add new order and add or update customer. The free side resolves the field map and fires bit_integrations_profilepress_*; all ProfilePress model work lives in Pro. Bit-Pi's four get*DetailsById methods are not ported — they are lookups rather than actions. The plan is per-flow configuration, so it is a fetched dropdown rather than a field-map row; everything the flow varies per run stays mappable. Plans are read through PlanRepository::retrieveAll() and held in component state so they are never serialized into flow_details. validateProfilePressConf covers what the field map cannot express, and both the create wizard and the edit screen call it, so neither can save a flow that would fail on every run. The plugin ships under the wp-user-avatar slug, which it kept after being renamed from WP User Avatar, so PPRESS_VERSION_NUMBER is the activation check rather than a directory name.
Replaces the customer_id field on add_new_order with customer_email. ProfilePress has no email lookup — CustomerFactory exposes only fromId() and fromUserId() — so the address is resolved through the WordPress account the customer is bound to. The numeric id is still what ppress_subscribe_user_to_plan needs, so it is read back off the resolved entity. Trigger data almost always carries an email rather than a ProfilePress customer id, which made the previous field awkward to map. Paired with the handler change in bit-integrations-pro; neither half works alone.
The plan dropdown rendered blank on the edit screen even though conf held the saved id. react-multiple-select-dropdown-lite matches defaultValue against its options in an effect keyed on defaultValue alone, never on the options. Plans are fetched after mount, so at match time the option list was still empty and the saved id matched nothing; when the plans arrived the match never re-ran because defaultValue had not changed. Keying the select on the option count remounts it once the plans land, so the saved value resolves. This only surfaced because the option lists were moved out of conf to keep them out of flow_details — integrations that persist their lists have them available at mount and never hit it.
Two fixes from review of the free side. mainAction defaulted to add_or_update_customer, so a flow that lost its mainAction would silently provision WordPress accounts from whatever was in the field map instead of failing. Both actions write and one creates users, so there is no safe default; a missing value now falls through to "Invalid action". The plan fetch treated an empty array as success, because [] is truthy. A site with no ProfilePress plans got a success toast beside an empty dropdown and no explanation for why the flow could not be saved. It now says no plans were found. Also reindents the plan dropdown options block back to what Prettier emits; formatting only, no behaviour change.
There was a problem hiding this comment.
Code Review
This pull request introduces a new integration for ProfilePress, adding backend controllers, record API helpers, routes, and frontend React components for authorization, configuration, and editing. The review feedback highlights a few key areas for improvement: rebuilding non-persisted configuration fields on mount when editing to prevent component crashes, declaring the refreshPlans controller method as static since it does not access instance state, and ensuring that the .catch block in the plans fetching function displays an error message to the user upon failure.
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.
| useEffect(() => { | ||
| if (listsForAction(mainAction).length > 0) { | ||
| refreshProfilePressPlans(setLists, setIsLoading) | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [mainAction]) |
There was a problem hiding this comment.
When editing an integration, ensure that non-persisted configuration fields (like static fields) are rebuilt on mount from the saved action without modifying the saved field mappings, to prevent empty options and component crashes.
useEffect(() => {
if (mainAction) {
setProfilePressConf(prevConf =>
create(prevConf, draftConf => {
if (!draftConf.profilePressFields || draftConf.profilePressFields.length === 0) {
draftConf.profilePressFields = fieldsByAction[mainAction] || []
}
})
)
}
if (listsForAction(mainAction).length > 0) {
refreshProfilePressPlans(setLists, setIsLoading)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainAction])References
- When editing an integration, ensure that non-persisted configuration fields (like static fields) are rebuilt on mount from the saved action without modifying the saved field mappings, to prevent empty options and component crashes.
There was a problem hiding this comment.
Not applying — the premise does not hold here, though there is a real issue nearby.
profilePressFields is persisted. It is written into conf when the action is chosen:
draftConf.profilePressFields = fieldsByAction[value] || []conf is serialized into btcbi_flow.flow_details on save, so the edit screen reads it back from the database and the field-map section renders. Both read sites are also guarded — {mainAction && profilePressConf.profilePressFields && (…)} and profilePressConf?.profilePressFields?.filter(…) || [] — so there is no crash path either.
The suggested guard only fires when the value is missing or empty, which in practice does not happen.
The genuine issue is staleness, not absence: because the definitions are persisted, a release that changes the field list leaves flows saved before it holding the old copy. This PR actually creates an instance of that — add_new_order swapped customer_id for customer_email, so a flow saved earlier still carries the old definition, and the proposed guard would not repair it since the array is non-empty.
Deriving profilePressFields from mainAction at render time would fix both, but it changes the saved config shape and applies to the whole component family rather than this integration, so it belongs in its own change.
| wp_send_json_success(true); | ||
| } | ||
|
|
||
| public function refreshPlans() |
There was a problem hiding this comment.
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.
public static function refreshPlans()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 — style preference rather than a defect, and consistent with the same call on #196 and #197.
Route::action() dispatches through reflection and supports both forms explicitly:
$response = $reflectionMethod->invoke($reflectionMethod->isStatic() ? null : new $invokeable[0](), $data);A non-static callback is instantiated and invoked correctly. There are 30 non-static refresh* methods across backend/Actions/ today, and this controller follows FluentCartController, whose refresh methods are also non-static.
Static is the wider majority (64 vs 30), so it is worth standardising — but as one sweep across the directory rather than on whichever integration is newest, which would otherwise leave BadgeOS, LatePoint and ProfilePress inconsistent with each other.
|
|
||
| toast.error(__('ProfilePress plans fetch failed. Please try again', 'bit-integrations')) | ||
| }) | ||
| .catch(() => setIsLoading(false)) |
There was a problem hiding this comment.
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.
.catch(() => {
setIsLoading(false)
toast.error(__('ProfilePress plans fetch failed. Please try again', 'bit-integrations'))
})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.
Agreed — applied. The catch reset the loading flag but said nothing, so a network failure left the spinner stopping beside an empty dropdown with no indication anything had gone wrong.
.catch(() => {
setIsLoading(false)
toast.error(__('ProfilePress plans fetch failed. Please try again', 'bit-integrations'))
})
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
The catch on the plan request reset the loading flag but reported nothing, so a network failure left the spinner stopping beside an empty dropdown with no indication anything had gone wrong. Shows the same error the non-ok response path already does.
Description
Adds ProfilePress as an action integration and registers it as a trigger source.
Two write actions — Add New Order and Add or Update Customer — plus the UI for
configuring them.
The free plugin resolves the field map and fires
bit_integrations_profilepress_*; all ProfilePress model work lives inbit-integrations-pro.
Motivation & Context
ProfilePress is a widely used membership and user-registration plugin that Bit-Pi
already integrates with, but Bit Integrations had no support for it. This closes
that gap so membership flows — subscribe a customer to a plan on form submit,
sync a member to a CRM when their subscription activates — can be built here.
Bit-Pi's four
get*DetailsByIdmethods are deliberately not ported: they arelookups, not actions.
Worth noting for reviewers: ProfilePress ships under the
wp-user-avatarslug,which it kept after being renamed from WP User Avatar, so the activation check is
PPRESS_VERSION_NUMBERrather than a directory name.Related Links: (if applicable)
Type of Change
Key Changes
Backend
ProfilePressControllerwith an activation check, an authorizeendpoint, and a plan dropdown endpoint backed by
PlanRepository::retrieveAll()RecordApiHelperdispatching both actions to their Pro hooks, loggingevery path through
LogHandler::saveProfilePresstoAllTriggersNameandcustomFormIntegrations,registering the Pro trigger with the flow builder
Frontend
integration layout, Utilities section, create and edit wizards
validateProfilePressConf, shared by the create wizard and the editscreen so neither can save a flow that would fail on every run
Fixes found in review
mainActiondefaulting toadd_or_update_customer, which meant aflow that lost its
mainActionsilently provisioned WordPress accounts insteadof failing
react-multiple-select-dropdown-litematchesdefaultValueagainst its optionsin an effect keyed on
defaultValuealone, and plans are fetched after mountthe flow could not be saved
Checklist
Notes for reviewers
against a live install; verification was static (
php -l, php-cs-fixer, ESLint,Prettier) plus hook-argument and field-key parity checks.
mainActionno longer has a fallback. Any existing flow with a blankmainActionwill now return "Invalid action" rather than creating a user —intended, but a live behaviour change.
Changelog