feat(instasent): add Instasent SMS action integration#184
Conversation
Ports the Instasent SaaS SMS integration into the free plugin (MailerLite pattern). All six actions are Pro-gated: the free plugin handles token auth, configuration and field mapping, then fires a per-action hook the Pro plugin implements. - backend/Actions/Instasent: InstasentController (token authorize + data source refresh), RecordApiHelper (fires bit_integrations_instasent_* hooks), Routes - frontend/AllIntegrations/Instasent: token-auth wizard + 6 actions (Send SMS, Create Lookup, Create Data Source, Create/Update Contact, Delete Contact, Create Contact Event). Project Id is a config input and Data Source is a fetched dropdown instead of field-map rows; other ids stay in the field map - register in NewInteg/EditInteg/IntegInfo/SelectAction + integ logo
- Field map: add a "Custom Field..." option for contact/event actions so custom contact attributes and event parameters can actually be entered (the Pro helper already routed non-reserved keys, but the UI offered none) - Fix the field-map required-row check (i < requiredFlds.length) and bind the select value to the field key - Clear the fetched data sources and the selected data source when the Project Id changes, so a stale selection from another project is not reused - Fix the dead custom-value guard in checkMappedFields (operator precedence) and reject an unfinished custom-key row - RecordApiHelper: use isset() instead of !is_null() when reading mapped trigger values to avoid undefined-index notices - Register the Instasent tutorial/doc link - Drop a no-op expression, an unused import, and a stray <br/>
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
There was a problem hiding this comment.
Code Review
This pull request introduces the Instasent integration, adding backend controllers, API helpers, and routes, alongside frontend components for authorization, configuration, and field mapping. Feedback on the backend focuses on robust error handling, specifically recommending is_wp_error() checks and null guards on request parameters to prevent fatal errors in PHP 8.0+, as well as simplifying redundant checks. On the frontend, the feedback suggests populating static fields on mount in the edit view to prevent component crashes, and applying optional chaining and defensive guards to avoid runtime TypeErrors when fields or datasources are undefined.
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.
| $response = HttpHelper::get($endpoint, null, $header); | ||
|
|
||
| if (HttpHelper::$responseCode == 200) { |
There was a problem hiding this comment.
Always validate the response with is_wp_error() before checking response codes or accessing properties to prevent fatal errors in PHP 8.0+.
$response = HttpHelper::get($endpoint, null, $header);
if (is_wp_error($response)) {
wp_send_json_error($response->get_error_message(), 400);
}
if (HttpHelper::$responseCode == 200) {References
- In PHP, when handling responses that may return a WP_Error object, always use the
is_wp_error()function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.
There was a problem hiding this comment.
Done — added an is_wp_error($response) guard right after the request, before reading the response code/properties.
| $response = HttpHelper::get($endpoint, null, $header); | ||
|
|
||
| if (HttpHelper::$responseCode == 200) { |
There was a problem hiding this comment.
Always validate the response with is_wp_error() before checking response codes or accessing properties to prevent fatal errors in PHP 8.0+.
$response = HttpHelper::get($endpoint, null, $header);
if (is_wp_error($response)) {
wp_send_json_error($response->get_error_message(), 400);
}
if (HttpHelper::$responseCode == 200) {References
- In PHP, when handling responses that may return a WP_Error object, always use the
is_wp_error()function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.
There was a problem hiding this comment.
Done — added an is_wp_error($response) guard right after the request, before reading the response code/properties.
| break; | ||
| } | ||
|
|
||
| $apiResponse = \is_array($apiResponse) ? (object) $apiResponse : $apiResponse; |
There was a problem hiding this comment.
Always validate the response with is_wp_error() before casting or accessing properties to prevent fatal errors in PHP 8.0+.
if (is_wp_error($apiResponse)) {
LogHandler::save(
$this->_integrationID,
wp_json_encode(['type' => 'action', 'type_name' => $typeName]),
'error',
wp_json_encode(['message' => $apiResponse->get_error_message()])
);
return $apiResponse;
}
$apiResponse = \is_array($apiResponse) ? (object) $apiResponse : $apiResponse;References
- In PHP, when handling responses that may return a WP_Error object, always use the
is_wp_error()function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.
There was a problem hiding this comment.
Done — added an is_wp_error() guard (with an error log) before casting $apiResponse.
| import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' | ||
| import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' | ||
| import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' | ||
| import { checkMappedFields, handleInput } from './InstasentCommonFunc' |
There was a problem hiding this comment.
Done — InstasentStaticData is now imported and used by the mount effect (see the related comment on the effect).
| useEffect(() => { | ||
| if (!instasentConf?.action) { | ||
| setInstasentConf(prev => | ||
| create(prev, draftConf => { | ||
| draftConf.action = 'send_sms' | ||
| }) | ||
| ) | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
Populate instasentFields on mount if the action is already set. This prevents React component crashes and ensures the field map renders correctly without wiping out the user's saved mappings.
useEffect(() => {
if (!instasentConf?.action) {
setInstasentConf(prev =>
create(prev, draftConf => {
draftConf.action = 'send_sms'
})
)
} else {
setInstasentConf(prev =>
create(prev, draftConf => {
draftConf.instasentFields = InstasentStaticData[instasentConf.action] || []
})
)
}
}, [])There was a problem hiding this comment.
Good catch — this was a real edit-mode bug. instasentFields isn't persisted, so on edit the field map had no options. The mount effect now rebuilds it from the saved action:
useEffect(() => {
setInstasentConf(prev =>
create(prev, draftConf => {
const action = draftConf.action || 'send_sms'
draftConf.action = action
draftConf.instasentFields = InstasentStaticData[action] || []
})
)
}, [])The saved field_map is left untouched.
| const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) || [] | ||
| const nonRequiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === false) || [] |
There was a problem hiding this comment.
Use optional chaining instasentConf?.instasentFields?.filter to prevent crashes if instasentFields is undefined on initial render.
| const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) || [] | |
| const nonRequiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === false) || [] | |
| const requiredFlds = instasentConf?.instasentFields?.filter(fld => fld.required === true) || [] | |
| const nonRequiredFlds = instasentConf?.instasentFields?.filter(fld => fld.required === false) || [] |
There was a problem hiding this comment.
Done — added optional chaining: instasentConf?.instasentFields?.filter(...).
|
|
||
| public function authorize($refreshFieldsRequestParams) | ||
| { | ||
| if (empty($refreshFieldsRequestParams->auth_token)) { |
There was a problem hiding this comment.
There was a problem hiding this comment.
Done — the guard now checks the request object itself before accessing properties: empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token).
|
|
||
| public function refreshDatasources($refreshFieldsRequestParams) | ||
| { | ||
| if (empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) { |
There was a problem hiding this comment.
Accessing properties on $refreshFieldsRequestParams when it is null or not an object will trigger a fatal error in PHP 8.0+. Guard against null/empty request parameters before accessing properties.
if (empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) {There was a problem hiding this comment.
Done — the guard now checks the request object itself before accessing properties: empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token).
| if (is_wp_error($instasentApiResponse)) { | ||
| return $instasentApiResponse; | ||
| } | ||
|
|
||
| return $instasentApiResponse; |
There was a problem hiding this comment.
Done — removed the redundant is_wp_error branch; the method now returns $instasentApiResponse directly.
| options={(instasentConf?.default?.datasources || []).map(ds => ({ | ||
| label: ds.name || ds.id, | ||
| value: String(ds.id) | ||
| }))} |
There was a problem hiding this comment.
Apply defensive programming guards to avoid runtime TypeErrors if ds is null or ds.id is undefined.
| options={(instasentConf?.default?.datasources || []).map(ds => ({ | |
| label: ds.name || ds.id, | |
| value: String(ds.id) | |
| }))} | |
| options={(instasentConf?.default?.datasources || []).map(ds => ({ | |
| label: ds?.name || ds?.id || '', | |
| value: String(ds?.id || '') | |
| }))} |
There was a problem hiding this comment.
Done — added null-safe guards: label: ds?.name || ds?.id || '', value: String(ds?.id || '').
- EditInstasent: rebuild instasentFields from the saved action on mount so the field map renders on edit (the field list is not persisted) - Controller/RecordApiHelper: guard responses with is_wp_error before reading the response code or casting, and guard empty request params before property access; drop a redundant is_wp_error branch - FieldMap/IntegLayout: optional chaining on instasentFields and null-safe data-source option mapping
…eckbox Remove allowUnicode from send_sms field mapping and replace it with a checkbox in a new Utilities section. Adds InstasentActions component and wires it through backend controller/record helper.
Description
Adds the Instasent SaaS SMS integration to the free plugin, following the MailerLite pattern. Six actions are Pro-gated: the free plugin handles token authentication, configuration and field mapping, then fires a per-action hook the Pro add-on implements.
Motivation & Context
Instasent is an SMS/marketing messaging platform. This lets Bit Integrations send SMS, run number lookups, create data sources, and push contacts/events into Instasent audiences as flow actions.
Type of Change
Key Changes
Backend (Actions/Instasent)
InstasentController(token authorize + data-source refresh) andRecordApiHelper, which maps the field map and firesbit_integrations_instasent_*hooks per action, plusRoutes.Frontend (AllIntegrations/Instasent)
Registration
Checklist
Changelog