Skip to content

feat(flodesk): add Flodesk integration - #214

Open
RishadAlam wants to merge 16 commits into
mainfrom
feat/flodesk
Open

feat(flodesk): add Flodesk integration#214
RishadAlam wants to merge 16 commits into
mainfrom
feat/flodesk

Conversation

@RishadAlam

Copy link
Copy Markdown
Member

Description

Adds Flodesk to Bit Integrations: 8 write actions with the connection, dropdown data and UI in Free, and the API calls in Pro. Pairs with Bit-Apps-Pro/bit-integrations-pro#144.

Motivation & Context

Ported from the Bit Flows (bit-pi) integration. Follows the Brilliant Directories and SureContact shape rather than the local-plugin templates, since Flodesk is a SaaS API: Free owns the connection and builds the ApiClient, and hands it to Pro to make the call.

Only write operations ship as actions — the read endpoints back dropdowns instead. Flodesk's triggers are external webhooks rather than WordPress hooks, so they are out of scope here.

Type of Change

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

Key Changes

Backend

  • Added FlodeskController with the connection config, four dropdown fetchers (segments, workflows, custom fields, segment colours) and the ApiClient factory
  • Added RecordApiHelper, which builds the payload and fires one hook per action, logging every path through LogHandler
  • Fixed every write returning 415. ApiClient::addHeaders() de-duplicates with array_unique(SORT_REGULAR) over an associative array, so it drops any header whose value repeats — Accept and Content-Type were both application/json and Content-Type was discarded. Sending no Accept keeps it alive; the underlying helper is untouched here
  • Fixed the Segments and Workflows dropdowns being silently empty: paged endpoints decode to an object while custom-fields/all and segments/colors return a bare array, and the row reader only understood the array form
  • Fixed $authConfig['fields'] writing a decrypted copy of the API key onto flow details that nothing reads

Core

  • Added ApiResponse::errorMessage(). getError() is null on a non-2xx, so an integration wanting a usable message has to reach into the body — three integrations now hand-roll the same chain. Additive, so no existing caller changes; Flodesk drops its copy and the other two are left for a follow-up

Frontend

  • Added the authorization step, action layout, field map, Utilities and edit screens, with every action pro-gated
  • Fixed account custom fields never reaching the mapping dropdown — the mappable list was snapshotted before the fetch resolved and never recomputed
  • Added a refresh control for custom fields, so one created in Flodesk after the node was opened can be pulled in
  • Fixed the step-two pane overlapping the Conditional Logics row: it was pinned to a fixed height with overflow visible, so content spilled out of its own box

Checklist

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

Testing

Driven against the live Flodesk API through the real Controller → hook → Pro handler path. 7 of 8 actions succeed; all validation refusals are correct; all four dropdowns populate. The two workflow actions reach draft_workflow_not_allowed because the test account has no published workflow — request shape and auth are confirmed correct.

Not verified: the UI has not been rendered in a browser. It compiles, lints and builds, but the authorization screen in particular is unexercised — every test connection was written directly to the database.

Changelog

  • New Integration: Flodesk — create or update subscribers, create segments and custom fields, add or remove subscribers from segments and workflows, and unsubscribe them

Free owns the connection and the dropdown data; the nine write actions are Pro,
so RecordApiHelper only builds the payload and fires the matching hook. The
ApiClient it hands over carries the Basic auth Flodesk wants — the API key as the
username with no password.

The four fetch endpoints exist because the corresponding Bit-Pi read actions are
not actions here: segments, workflows, custom fields and segment colours all back
selects instead. Colours have to be fetched rather than hardcoded, since Flodesk
only accepts values from its own palette.

Paged endpoints wrap rows in `data` while custom-fields/all and segments/colors
answer with a bare array, so the list reader handles both.
Three-step wizard cloned from BrilliantDirectories: connection, action plus field
mapping, then the shared step three.

Field maps hold only what has to come from trigger data — the identifier of the
record an action targets, and free text. Segments, the segment colour and the
workflow are fetched selects instead, because they are configuration choices
rather than the target of the action; putting them in the map would force the
user to type a magic id, and putting the target id in a dropdown would pin the
action to one record.

Create or Update Subscriber also merges the account's own custom fields into its
mappable list, so those can be written without being declared here. Its segments
and double opt-in are optional, so they sit in Utilities behind a checkbox rather
than in the always-visible layout.

Every action is gated with is_pro, the logo is generated from the Bit-Pi artwork.
Lazy import plus a case in the new, edit and info switches, and an entry in the
action picker so the integration is reachable.
Leaves only the translators pragma, which phpcs requires beside the %s
placeholder. API behaviour the comments recorded — the literal "@" in subscriber
paths, the mandatory segment colour, the error array getAccessToken returns, and
Flodesk creating duplicate webhooks — stays in the commit history.
Two faults found running the actions against the live API for the first time.

Every write returned 415. ApiClient::addHeaders() de-duplicates with
array_unique(SORT_REGULAR) over an associative array, so it drops any header
whose value repeats — Accept and Content-Type were both application/json and
Content-Type was the one discarded. Sending no Accept keeps Content-Type alive.
Nothing here can fix the underlying helper; an integration only has to avoid
giving two headers the same value.

The Segments and Workflows dropdowns were silently empty. Paged endpoints decode
to an object while custom-fields/all and segments/colors answer with a bare
array, and the row reader only understood the array form, so it fell through and
returned nothing for exactly the two lists that page.
The custom fields an account defines never reached the mapping dropdown. They
were fetched when the action was chosen, but the mappable list had already been
snapshotted into conf.flodeskFields a moment earlier, and nothing recomputed it
when the request came back — so only email, first name and last name were ever
offered.

The field map now derives its options from the current config instead of that
snapshot, which makes the fetched fields appear as soon as they arrive. The
snapshot is gone rather than kept in sync, since it had no other reader.

Adds the refresh control next to the Flodesk Fields heading, so a field created
in Flodesk after the node was opened can be pulled in without re-picking the
action.
It only ever worked when the subscribers value arrived as a JSON string: a
mapped array is flattened to [0 => NULL] by the custom-value helper before the
action sees it, so the array branch was unreachable in practice.

Taken out end to end rather than left half-wired — the machine and its root
entry, the module and field map, the dispatch case, the hook registration, the
handler, and the batch limit. The JSON decoding and field-map readers that
existed only for it go with it.
The pane was pinned to height 500 with overflow visible, so once the Utilities
checkboxes pushed the layout past that, the content spilled out of the box and
landed on top of the Conditional Logics row that IntegrationStepThree renders
below it.

Height now grows with the content, with 500 kept as a minimum so a short action
still gets a full pane. Overflow stays visible so select dropdowns can escape
the box. Also reserves space above the Next button, which is floated and so
contributes no height of its own.
$authConfig['fields'] maps a credential onto a flat property of flow_details or
the request params, for integrations whose code reads it there instead of from
the connection. Flodesk reads nothing that way: the controller resolves the
connection and ApiClient applies the credential, so mapping api_key to username
only wrote a decrypted copy of the key onto an object nobody consulted.

Declaring an empty map keeps the rest of $authConfig doing its job — the slug
still binds a connection_id to this integration during injection.
bodyMessage existed because Brilliant Directories needs it: that API answers 200
with a status of error, and its message field carries the rows on a good list
call, so a type check is the only way to tell a reason from a payload. Flodesk
returns real status codes, keeps rows in data, and only ever puts a string in
message, so the separate method guarded an assertion that does not hold here.

The body read stays — ApiClient leaves getError null on a non-2xx, so without it
every API failure would report "Could not reach Flodesk" instead of what Flodesk
actually said. failureReason is private now; nothing outside the controller
calls it.
getError is null on a non-2xx, so an integration wanting a usable message has to
reach into the body — three of them now hand-roll the same chain, differing only
in the app name. errorMessage moves it onto the response, next to getValue,
which justifies itself the same way: every caller would otherwise repeat it.

It stays out of the 2xx-with-an-error-body case, since success() already puts
that on the caller, and Brilliant Directories is the only integration that needs
it.

Additive, so no existing caller changes. Flodesk drops its copy; SureContact and
Brilliant Directories keep theirs for now — they are unrelated to this branch,
and their duplication is harmless while it waits.
Copilot AI lite review requested due to automatic review settings August 27, 2026 08:39

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.

@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 August 27, 2026 09:10

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