Skip to content

feat(instasent): add Instasent SMS action integration#184

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

feat(instasent): add Instasent SMS action integration#184
RishadAlam wants to merge 5 commits into
mainfrom
feat/instasent

Conversation

@RishadAlam

Copy link
Copy Markdown
Member

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

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

Key Changes

Backend (Actions/Instasent)

  • Added InstasentController (token authorize + data-source refresh) and RecordApiHelper, which maps the field map and fires bit_integrations_instasent_* hooks per action, plus Routes.

Frontend (AllIntegrations/Instasent)

  • Added a token-auth wizard and six actions: Send SMS, Create Lookup, Create Data Source, Create/Update Contact, Delete Contact, Create Contact Event.
  • Added a Project Id config input and a fetched Data Source dropdown instead of mapping those ids in the field map; other ids (user id, event id, etc.) stay in the field map.
  • Added a Custom Field option in the field map so contact custom attributes and event parameters can be entered.

Registration

  • Updated NewInteg / EditInteg / IntegInfo / SelectAction and added the integration logo and tutorial link.

Checklist

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

Changelog

  • New Actions
    • Instasent: Send SMS, Create Lookup, Create Data Source, Create/Update Contact, Delete Contact, Create Contact Event.

Requires the matching Bit Integrations Pro PR, which implements the action logic.

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/>
@github-actions

github-actions Bot commented Jun 18, 2026

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

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

Comment on lines +38 to +40
$response = HttpHelper::get($endpoint, null, $header);

if (HttpHelper::$responseCode == 200) {

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

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

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.

Done — added an is_wp_error($response) guard right after the request, before reading the response code/properties.

Comment on lines +69 to +71
$response = HttpHelper::get($endpoint, null, $header);

if (HttpHelper::$responseCode == 200) {

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

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

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.

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;

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

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

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.

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'

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

Import InstasentStaticData to allow populating static fields on mount.

Suggested change
import { checkMappedFields, handleInput } from './InstasentCommonFunc'
import { checkMappedFields, handleInput, InstasentStaticData } from './InstasentCommonFunc'

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.

Done — InstasentStaticData is now imported and used by the mount effect (see the related comment on the effect).

Comment on lines +48 to +56
useEffect(() => {
if (!instasentConf?.action) {
setInstasentConf(prev =>
create(prev, draftConf => {
draftConf.action = 'send_sms'
})
)
}
}, [])

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

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] || []
        })
      )
    }
  }, [])

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.

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.

Comment on lines +14 to +15
const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) || []
const nonRequiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === 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.

high

Use optional chaining instasentConf?.instasentFields?.filter to prevent crashes if instasentFields is undefined on initial render.

Suggested change
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) || []

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.

Done — added optional chaining: instasentConf?.instasentFields?.filter(...).


public function authorize($refreshFieldsRequestParams)
{
if (empty($refreshFieldsRequestParams->auth_token)) {

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

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

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.

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

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

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

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.

Done — the guard now checks the request object itself before accessing properties: empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token).

Comment on lines +118 to +122
if (is_wp_error($instasentApiResponse)) {
return $instasentApiResponse;
}

return $instasentApiResponse;

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

The is_wp_error check here is redundant because both branches return $instasentApiResponse. Simplify the code by directly returning the response.

        return $instasentApiResponse;

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.

Done — removed the redundant is_wp_error branch; the method now returns $instasentApiResponse directly.

Comment on lines +127 to +130
options={(instasentConf?.default?.datasources || []).map(ds => ({
label: ds.name || ds.id,
value: String(ds.id)
}))}

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

Apply defensive programming guards to avoid runtime TypeErrors if ds is null or ds.id is undefined.

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

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.

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

1 participant