Skip to content

feat(profilepress): add ProfilePress membership integration#198

Open
RishadAlam wants to merge 5 commits into
mainfrom
feat/profilepress
Open

feat(profilepress): add ProfilePress membership integration#198
RishadAlam wants to merge 5 commits into
mainfrom
feat/profilepress

Conversation

@RishadAlam

@RishadAlam RishadAlam commented Jul 19, 2026

Copy link
Copy Markdown
Member

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 in
bit-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*DetailsById methods are deliberately not ported: they are
lookups, not actions.

Worth noting for reviewers: ProfilePress ships under the wp-user-avatar slug,
which it kept after being renamed from WP User Avatar, so the activation check is
PPRESS_VERSION_NUMBER rather than a directory name.

Related Links: (if applicable)

  • Pro counterpart: Bit-Apps-Pro/bit-integrations-pro#136 — neither half is functional alone, so the two should merge together

Type of Change

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

Key Changes

Backend

  • Added ProfilePressController with an activation check, an authorize
    endpoint, and a plan dropdown endpoint backed by PlanRepository::retrieveAll()
  • Added RecordApiHelper dispatching both actions to their Pro hooks, logging
    every path through LogHandler::save
  • Added ProfilePress to AllTriggersName and customFormIntegrations,
    registering the Pro trigger with the flow builder

Frontend

  • Added the ProfilePress integration UI — authorization, field map,
    integration layout, Utilities section, create and edit wizards
  • Added validateProfilePressConf, shared by the create wizard and the edit
    screen so neither can save a flow that would fail on every run

Fixes found in review

  • Fixed mainAction defaulting to add_or_update_customer, which meant a
    flow that lost its mainAction silently provisioned WordPress accounts instead
    of failing
  • Fixed the plan dropdown rendering blank when editing a saved flow —
    react-multiple-select-dropdown-lite matches defaultValue against its options
    in an effect keyed on defaultValue alone, and plans are fetched after mount
  • Fixed an empty plan list reporting success, leaving no explanation for why
    the flow could not be saved

Checklist

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

Notes for reviewers

  • Not runtime-tested. No test suite exists and no order or customer was written
    against a live install; verification was static (php -l, php-cs-fixer, ESLint,
    Prettier) plus hook-argument and field-key parity checks.
  • mainAction no longer has a fallback. Any existing flow with a blank
    mainAction will now return "Invalid action" rather than creating a user —
    intended, but a live behaviour change.

Changelog

  • New Actions
  • ProfilePress: 2 new actions added (Pro).
  • New Triggers
  • ProfilePress: 2 new events added (Pro).

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.
Copilot AI review requested due to automatic review settings July 19, 2026 05:45

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

Comment on lines +47 to +52
useEffect(() => {
if (listsForAction(mainAction).length > 0) {
refreshProfilePressPlans(setLists, setIsLoading)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainAction])

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 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
  1. 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.

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 — 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()

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

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
  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 — 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))

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

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'))
})

@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

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.
Copilot AI review requested due to automatic review settings July 19, 2026 05:53

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