From 68639307387f86644f6ab4d873e30a13a63aed65 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 18 Jun 2026 16:08:58 +0600 Subject: [PATCH 1/4] feat(instasent): add Instasent SMS action integration 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 --- .../Actions/Instasent/InstasentController.php | 124 +++++++++++ backend/Actions/Instasent/RecordApiHelper.php | 126 +++++++++++ backend/Actions/Instasent/Routes.php | 11 + .../components/AllIntegrations/EditInteg.jsx | 3 + .../Instasent/EditInstasent.jsx | 111 +++++++++ .../AllIntegrations/Instasent/Instasent.jsx | 132 +++++++++++ .../Instasent/InstasentAuthorization.jsx | 120 ++++++++++ .../Instasent/InstasentCommonFunc.js | 183 +++++++++++++++ .../Instasent/InstasentFieldMap.jsx | 100 +++++++++ .../Instasent/InstasentIntegLayout.jsx | 210 ++++++++++++++++++ .../Instasent/IntegrationHelpers.jsx | 27 +++ .../components/AllIntegrations/IntegInfo.jsx | 3 + .../components/AllIntegrations/NewInteg.jsx | 10 + .../src/components/Flow/New/SelectAction.jsx | 1 + .../src/resource/img/integ/instasent.webp | Bin 0 -> 3588 bytes 15 files changed, 1161 insertions(+) create mode 100644 backend/Actions/Instasent/InstasentController.php create mode 100644 backend/Actions/Instasent/RecordApiHelper.php create mode 100644 backend/Actions/Instasent/Routes.php create mode 100644 frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx create mode 100644 frontend/src/components/AllIntegrations/Instasent/Instasent.jsx create mode 100644 frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx create mode 100644 frontend/src/resource/img/integ/instasent.webp diff --git a/backend/Actions/Instasent/InstasentController.php b/backend/Actions/Instasent/InstasentController.php new file mode 100644 index 000000000..4de0d4cd1 --- /dev/null +++ b/backend/Actions/Instasent/InstasentController.php @@ -0,0 +1,124 @@ +auth_token)) { + wp_send_json_error( + __( + 'Requested parameter is empty', + 'bit-integrations' + ), + 400 + ); + } + + $endpoint = self::$_baseUrl . '/organization/account'; + $header = [ + 'Authorization' => 'Bearer ' . $refreshFieldsRequestParams->auth_token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + $response = HttpHelper::get($endpoint, null, $header); + + if (HttpHelper::$responseCode == 200) { + wp_send_json_success('Authorization Successful', 200); + + return; + } + + wp_send_json_error( + $response->message ?? $response ?? 'Authorization Failed', + 400 + ); + } + + public function refreshDatasources($refreshFieldsRequestParams) + { + if (empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) { + wp_send_json_error( + __('Requested parameter is empty', 'bit-integrations'), + 400 + ); + } + + $project = rawurlencode($refreshFieldsRequestParams->projectId); + $endpoint = self::$_baseUrl . "/v1/project/{$project}/datasource"; + $header = [ + 'Authorization' => 'Bearer ' . $refreshFieldsRequestParams->auth_token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + $response = HttpHelper::get($endpoint, null, $header); + + if (HttpHelper::$responseCode == 200) { + $entities = $response->entities ?? []; + $formattedResponse = []; + foreach ($entities as $entity) { + $formattedResponse[] = [ + 'id' => $entity->id ?? '', + 'name' => $entity->name ?? ($entity->id ?? ''), + ]; + } + + wp_send_json_success($formattedResponse, 200); + + return; + } + + wp_send_json_error( + $response->message ?? $response ?? __('Failed to fetch data sources', 'bit-integrations'), + 400 + ); + } + + public function execute($integrationData, $fieldValues) + { + $integrationDetails = $integrationData->flow_details; + $integId = $integrationData->id; + $auth_token = $integrationDetails->auth_token ?? ''; + $fieldMap = $integrationDetails->field_map ?? ''; + $action = $integrationDetails->action ?? ''; + + if ( + empty($fieldMap) + || empty($auth_token) + || empty($action) + ) { + // translators: %s: Placeholder value + return new WP_Error('REQ_FIELD_EMPTY', wp_sprintf(__('module, fields are required for %s api', 'bit-integrations'), 'Instasent')); + } + + $recordApiHelper = new RecordApiHelper($auth_token, $integrationDetails, $integId); + $instasentApiResponse = $recordApiHelper->execute( + $integrationDetails, + $fieldValues, + $fieldMap, + $auth_token, + $action + ); + + if (is_wp_error($instasentApiResponse)) { + return $instasentApiResponse; + } + + return $instasentApiResponse; + } +} diff --git a/backend/Actions/Instasent/RecordApiHelper.php b/backend/Actions/Instasent/RecordApiHelper.php new file mode 100644 index 000000000..08d516094 --- /dev/null +++ b/backend/Actions/Instasent/RecordApiHelper.php @@ -0,0 +1,126 @@ +_authToken = $auth_token; + $this->_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function generateReqDataFromFieldMap($data, $fieldMap) + { + $dataFinal = []; + + foreach ($fieldMap as $key => $value) { + $triggerValue = $value->formField; + $actionValue = $value->instasentFormField; + + if (empty($actionValue)) { + continue; + } + + if ($triggerValue === 'custom' && isset($value->customValue)) { + $dataFinal[$actionValue] = Common::replaceFieldWithValue($value->customValue, $data); + } elseif (!\is_null($data[$triggerValue])) { + $dataFinal[$actionValue] = $data[$triggerValue]; + } + } + + return $dataFinal; + } + + public function execute( + $integrationDetails, + $fieldValues, + $fieldMap, + $auth_token, + $action + ) { + $fieldData = $this->generateReqDataFromFieldMap($fieldValues, $fieldMap); + + $default = [ + 'success' => false, + 'message' => __('Bit Integrations Pro is required.', 'bit-integrations'), + ]; + + switch ($action) { + case 'send_sms': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_send_sms'), $default, $fieldData, $auth_token); + $typeName = 'send-sms'; + + break; + + case 'create_lookup': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_lookup'), $default, $fieldData, $auth_token); + $typeName = 'create-lookup'; + + break; + + case 'create_datasource': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_datasource'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-datasource'; + + break; + + case 'create_or_update_contact': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_or_update_contact'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-or-update-contact'; + + break; + + case 'delete_contact': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_delete_contact'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'delete-contact'; + + break; + + case 'create_contact_event': + $apiResponse = Hooks::apply(Config::withPrefix('instasent_create_contact_event'), $default, $fieldData, $integrationDetails, $auth_token); + $typeName = 'create-contact-event'; + + break; + + default: + $apiResponse = $default; + $typeName = $action; + + break; + } + + $apiResponse = \is_array($apiResponse) ? (object) $apiResponse : $apiResponse; + + if ( + (isset($apiResponse->success) && $apiResponse->success) + || isset($apiResponse->id) + || isset($apiResponse->data) + ) { + LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'action', 'type_name' => $typeName]), 'success', wp_json_encode($apiResponse)); + } else { + LogHandler::save($this->_integrationID, wp_json_encode(['type' => 'action', 'type_name' => $typeName]), 'error', wp_json_encode($apiResponse)); + } + + return $apiResponse; + } +} diff --git a/backend/Actions/Instasent/Routes.php b/backend/Actions/Instasent/Routes.php new file mode 100644 index 000000000..a1378dab5 --- /dev/null +++ b/backend/Actions/Instasent/Routes.php @@ -0,0 +1,11 @@ + import('./Acumbamail/EditAcumbamail')) const EditGroundhogg = lazy(() => import('./Groundhogg/EditGroundhogg')) const EditSendFox = lazy(() => import('./SendFox/EditSendFox')) const EditMailerLite = lazy(() => import('./MailerLite/EditMailerLite')) +const EditInstasent = lazy(() => import('./Instasent/EditInstasent')) const EditVbout = lazy(() => import('./Vbout/EditVbout')) const EditWhatsApp = lazy(() => import('./WhatsApp/EditWhatsApp')) const EditLearnDash = lazy(() => import('./LearnDash/EditLearnDash')) @@ -397,6 +398,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'MailerLite': return + case 'Instasent': + return case 'Vbout': return case 'WhatsApp': diff --git a/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx new file mode 100644 index 000000000..326ae6c13 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx @@ -0,0 +1,111 @@ +/* eslint-disable no-param-reassign */ + +import { create } from 'mutative' +import { useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router' +import { useRecoilState, useRecoilValue } from 'recoil' +import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' +import { checkMappedFields, handleInput } from './InstasentCommonFunc' +import InstasentIntegLayout from './InstasentIntegLayout' + +function EditInstasent({ allIntegURL }) { + const navigate = useNavigate() + const { id } = useParams() + const [flow, setFlow] = useRecoilState($newFlow) + const [instasentConf, setInstasentConf] = useRecoilState($actionConf) + const [isLoading, setIsLoading] = useState(false) + const [name, setName] = useState(instasentConf?.name || '') + const [loading, setLoading] = useState({ + list: false, + field: false, + auth: false + }) + const [snack, setSnackbar] = useState({ show: false }) + const formField = useRecoilValue($formFields) + + const saveConfig = () => { + if (!checkMappedFields(instasentConf)) { + setSnackbar({ show: true, msg: __('Please map mandatory fields', 'bit-integrations') }) + return + } + + saveActionConf({ + flow, + allIntegURL, + conf: instasentConf, + navigate, + edit: 1, + setLoading, + setSnackbar + }) + } + + useEffect(() => { + if (!instasentConf?.action) { + setInstasentConf(prev => + create(prev, draftConf => { + draftConf.action = 'send_sms' + }) + ) + } + }, []) + + const handleEditIntegName = e => { + setName(e.target.value) + + setInstasentConf(prevConf => + create(prevConf, draftConF => { + draftConF.name = e.target.value + }) + ) + } + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} + +
+
+ + + handleInput(e, instasentConf, setInstasentConf, loading, setLoading)} + instasentConf={instasentConf} + setInstasentConf={setInstasentConf} + loading={loading} + setLoading={setLoading} + setSnackbar={setSnackbar} + /> + + +
+
+ ) +} + +export default EditInstasent diff --git a/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx new file mode 100644 index 000000000..c915a4f85 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx @@ -0,0 +1,132 @@ +/* eslint-disable no-unused-expressions */ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import toast from 'react-hot-toast' +import { useNavigate } from 'react-router' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import Steps from '../../Utilities/Steps' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import InstasentAuthorization from './InstasentAuthorization' +import { checkMappedFields, handleInput } from './InstasentCommonFunc' +import InstasentIntegLayout from './InstasentIntegLayout' + +function Instasent({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(false) + const [loading, setLoading] = useState({ + list: false, + field: false, + auth: false, + datasource: false + }) + + const [step, setstep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + + const [instasentConf, setInstasentConf] = useState({ + name: 'Instasent', + type: 'Instasent', + auth_token: '', + action: '', + projectId: '', + datasourceId: '', + instasentFields: [], + field_map: [{ formField: '', instasentFormField: '' }], + default: { datasources: [] } + }) + + const saveConfig = () => { + setIsLoading(true) + const resp = saveIntegConfig( + flow, + setFlow, + allIntegURL, + instasentConf, + navigate, + '', + '', + setIsLoading + ) + resp.then(res => { + if (res.success) { + toast.success(res.data?.msg) + navigate(allIntegURL) + } else { + toast.error(res.data || res) + } + }) + } + + const nextPage = pageNo => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (!checkMappedFields(instasentConf)) { + toast.error(__('Please map mandatory fields', 'bit-integrations')) + return + } + instasentConf.field_map.length > 0 && setstep(pageNo) + } + + return ( +
+ +
+ +
+ + {/* STEP 1 */} + + + + {/* STEP 2 */} +
+ handleInput(e, instasentConf, setInstasentConf, loading, setLoading)} + instasentConf={instasentConf} + setInstasentConf={setInstasentConf} + loading={loading} + setLoading={setLoading} + setSnackbar={setSnackbar} + /> + + {instasentConf?.action && ( + + )} +
+ + {/* STEP 3 */} + saveConfig()} + isLoading={isLoading} + dataConf={instasentConf} + setDataConf={setInstasentConf} + formFields={formFields} + /> +
+ ) +} + +export default Instasent diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx new file mode 100644 index 000000000..2e7e16e93 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx @@ -0,0 +1,120 @@ +/* eslint-disable no-unused-expressions */ +import { useState } from 'react' +import { __ } from '../../../Utils/i18nwrap' +import LoaderSm from '../../Loaders/LoaderSm' +import Note from '../../Utilities/Note' +import TutorialLink from '../../Utilities/TutorialLink' +import { authorization } from './InstasentCommonFunc' + +export default function InstasentAuthorization({ + instasentConf, + setInstasentConf, + step, + setstep, + loading, + setLoading, + setSnackbar, + isInfo +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const [error, setError] = useState({ name: '', auth_token: '' }) + const url = 'https://app.instasent.com/' + + const nextPage = () => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + !instasentConf?.default + setstep(2) + } + + const handleInput = e => { + const newConf = { ...instasentConf } + const rmError = { ...error } + rmError[e.target.name] = '' + newConf[e.target.name] = e.target.value + setError(rmError) + setInstasentConf(newConf) + } + + const note = ` +

${__('Step of generate API token:', 'bit-integrations')}

+
    +
  • ${__('Goto', 'bit-integrations')} Instasent Dashboard
  • +
  • ${__( + 'Copy the API Token and paste into API Token field of your authorization form.', + 'bit-integrations' + )}
  • +
  • ${__('Finally, click Authorize button.', 'bit-integrations')}
  • +
+ ` + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} +
+ + +
+ {__('API Token:', 'bit-integrations')} +
+ +
{error.auth_token}
+ + + {__('To Get API Token, Please Visit', 'bit-integrations')} +   + + {__('Instasent Dashboard', 'bit-integrations')} + + +
+
+ + {!isInfo && ( +
+ +
+ +
+ )} + +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js new file mode 100644 index 000000000..1890104cd --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js @@ -0,0 +1,183 @@ +/* eslint-disable no-else-return */ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' + +// Actions that need a Project / Data Source. These ids are selected at config time +// (Project Id as text, Data Source as a fetched dropdown) instead of in the field map. +export const ACTIONS_WITH_PROJECT = [ + 'create_datasource', + 'create_or_update_contact', + 'delete_contact', + 'create_contact_event' +] + +export const ACTIONS_WITH_DATASOURCE = [ + 'create_or_update_contact', + 'delete_contact', + 'create_contact_event' +] + +export const InstasentStaticData = { + send_sms: [ + { key: 'from', label: 'From', required: true }, + { key: 'to', label: 'To', required: true }, + { key: 'text', label: 'Text', required: true }, + { key: 'clientId', label: 'Client Id', required: false }, + { key: 'allowUnicode', label: 'Allow Unicode', required: false } + ], + create_lookup: [{ key: 'to', label: 'To', required: true }], + create_datasource: [ + { key: 'name', label: 'Name', required: true }, + { key: 'description', label: 'Description', required: false }, + { key: 'defaultCountry', label: 'Default Country', required: false }, + { key: 'locale', label: 'Locale', required: false }, + { key: 'timezone', label: 'Timezone', required: false } + ], + create_or_update_contact: [ + { key: 'userId', label: 'User Id', required: true }, + { key: 'firstName', label: 'First Name', required: false }, + { key: 'lastName', label: 'Last Name', required: false }, + { key: 'email', label: 'Email', required: false }, + { key: 'phoneMobile', label: 'Phone Mobile', required: false } + ], + delete_contact: [{ key: 'userId', label: 'User Id', required: true }], + create_contact_event: [ + { key: 'userId', label: 'User Id', required: true }, + { key: 'eventId', label: 'Event Id', required: true }, + { key: 'eventType', label: 'Event Type', required: true } + ] +} + +export const handleInput = (e, instasentConf, setInstasentConf, loading, setLoading) => { + const { name, value } = e.target + + const updatedConf = create(instasentConf, draftConf => { + draftConf[name] = value + }) + + setInstasentConf(updatedConf) + + if (name === 'action' && value !== '') { + instasentRefreshFields(updatedConf, setInstasentConf, loading, setLoading) + } +} + +export const generateMappedField = instasentConf => { + const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ + formField: '', + instasentFormField: field.key + })) + : [{ formField: '', instasentFormField: '' }] +} + +export const checkMappedFields = instasentConf => { + if (ACTIONS_WITH_PROJECT.includes(instasentConf?.action) && !instasentConf?.projectId) { + return false + } + + if (ACTIONS_WITH_DATASOURCE.includes(instasentConf?.action) && !instasentConf?.datasourceId) { + return false + } + + const mappedFields = instasentConf?.field_map + ? instasentConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.instasentFormField || + (!mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + if (mappedFields.length > 0) { + return false + } + return true +} + +export const authorization = (confTmp, setIsAuthorized, loading, setLoading) => { + if (!confTmp.auth_token) { + toast.error(__("API Token can't be empty", 'bit-integrations')) + + return + } + + setLoading({ ...loading, auth: true }) + + const requestParams = { + auth_token: confTmp.auth_token + } + + bitsFetch(requestParams, 'instasent_authorize').then(result => { + setLoading({ ...loading, auth: false }) + + if (result && result.success) { + setIsAuthorized(true) + + toast.success(__('Authorized Successfully', 'bit-integrations')) + + return + } + + toast.error(__('Authorized failed', 'bit-integrations')) + }) +} + +export const instasentRefreshFields = (confTmp, setConf, loading, setLoading) => { + setLoading({ ...loading, field: true }) + + const staticFields = InstasentStaticData[confTmp?.action] || [] + + setConf(prev => + create(prev, draftConf => { + draftConf.instasentFields = staticFields + draftConf.field_map = generateMappedField(draftConf) + }) + ) + + setLoading({ ...loading, field: false }) + + toast.success(__('Fields refresh successfully', 'bit-integrations')) +} + +export const refreshDatasources = (confTmp, setConf, loading, setLoading) => { + if (!confTmp.auth_token) { + toast.error(__("API Token can't be empty", 'bit-integrations')) + + return + } + + if (!confTmp.projectId) { + toast.error(__('Please enter a Project Id first', 'bit-integrations')) + + return + } + + setLoading({ ...loading, datasource: true }) + + const requestParams = { + auth_token: confTmp.auth_token, + projectId: confTmp.projectId + } + + bitsFetch(requestParams, 'refresh_instasent_datasources').then(result => { + setLoading({ ...loading, datasource: false }) + + if (result && result.success) { + setConf(prev => + create(prev, draftConf => { + draftConf.default = draftConf.default || {} + draftConf.default.datasources = result.data || [] + }) + ) + + toast.success(__('Data sources fetched successfully', 'bit-integrations')) + + return + } + + toast.error(__('Data sources fetch failed', 'bit-integrations')) + }) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx new file mode 100644 index 000000000..5cd98282d --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx @@ -0,0 +1,100 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { handleCustomValue } from '../IntegrationHelpers/IntegrationHelpers' +import TagifyInput from '../../Utilities/TagifyInput' +import { addFieldMap, delFieldMap, handleFieldMapping } from './IntegrationHelpers' + +export default function InstasentFieldMap({ i, formFields, field, instasentConf, setInstasentConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) || [] + const nonRequiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, instasentConf, setInstasentConf)} + label={__('Custom Value', 'bit-integrations')} + className="mr-2" + type="text" + value={field.customValue} + placeholder={__('Custom Value', 'bit-integrations')} + formFields={formFields} + /> + )} + + +
+ {i >= requiredFlds.length && ( + <> + + + + )} +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx new file mode 100644 index 000000000..67f3b5e99 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx @@ -0,0 +1,210 @@ +import { create } from 'mutative' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from './IntegrationHelpers' +import { + ACTIONS_WITH_DATASOURCE, + ACTIONS_WITH_PROJECT, + instasentRefreshFields, + refreshDatasources +} from './InstasentCommonFunc' +import InstasentFieldMap from './InstasentFieldMap' + +const actionOptions = [ + { + value: 'send_sms', + label: __('Send SMS', 'bit-integrations'), + isPro: true + }, + { + value: 'create_lookup', + label: __('Create Lookup', 'bit-integrations'), + isPro: true + }, + { + value: 'create_datasource', + label: __('Create Datasource', 'bit-integrations'), + isPro: true + }, + { + value: 'create_or_update_contact', + label: __('Create or Update Contact', 'bit-integrations'), + isPro: true + }, + { + value: 'delete_contact', + label: __('Delete Contact', 'bit-integrations'), + isPro: true + }, + { + value: 'create_contact_event', + label: __('Create Contact Event', 'bit-integrations'), + isPro: true + } +] + +export default function InstasentIntegLayout({ + formFields, + handleInput, + instasentConf, + setInstasentConf, + loading, + setLoading, + setSnackbar +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const handleMainAction = value => { + const updatedConf = create(instasentConf, draftConf => { + draftConf.action = value + }) + + setInstasentConf(updatedConf) + + if (value !== '') { + instasentRefreshFields(updatedConf, setInstasentConf, loading, setLoading) + } + } + + const setDatasource = value => { + setInstasentConf(prev => + create(prev, draftConf => { + draftConf.datasourceId = value + }) + ) + } + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={actionOptions.map(action => ({ + label: checkIsPro(isPro, action.isPro) ? action.label : getProLabel(action.label), + value: action.value, + disabled: !checkIsPro(isPro, action.isPro) + }))} + singleSelect + closeOnSelect + /> +
+
+ + {ACTIONS_WITH_PROJECT.includes(instasentConf?.action) && ( +
+ {__('Project Id:', 'bit-integrations')} + +
+ )} +
+ {ACTIONS_WITH_DATASOURCE.includes(instasentConf?.action) && ( +
+ {__('Data Source:', 'bit-integrations')} + setDatasource(val)} + options={(instasentConf?.default?.datasources || []).map(ds => ({ + label: ds.name || ds.id, + value: String(ds.id) + }))} + singleSelect + closeOnSelect + /> + +
+ )} + + {loading.field && ( + + )} + + {instasentConf?.action && !loading?.field && ( + <> +
+ + {__('Field Map', 'bit-integrations')} + + +
+
+
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('Instasent Fields', 'bit-integrations')} +
+
+ + {instasentConf?.field_map.map((itm, i) => ( + + ))} +
+ +
+
+
+ + )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx new file mode 100644 index 000000000..b9bbc114e --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx @@ -0,0 +1,27 @@ +/* eslint-disable no-unused-expressions */ +import { __ } from '../../../Utils/i18nwrap' + +export const addFieldMap = (i, confTmp, setConf) => { + const newConf = { ...confTmp } + newConf.field_map.splice(i, 0, {}) + setConf({ ...newConf }) +} + +export const delFieldMap = (i, confTmp, setConf) => { + const newConf = { ...confTmp } + if (newConf.field_map.length > 1) { + newConf.field_map.splice(i, 1) + } + + setConf({ ...newConf }) +} + +export const handleFieldMapping = (event, index, conftTmp, setConf) => { + const newConf = { ...conftTmp } + newConf.field_map[index][event.target.name] = event.target.value + + if (event.target.value === 'custom') { + newConf.field_map[index].customValue = '' + } + setConf({ ...newConf }) +} diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index c095b228d..340e89a5b 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -68,6 +68,7 @@ const GroundhoggAuthorization = lazy(() => import('./Groundhogg/GroundhoggAuthor const SendFoxAuthorization = lazy(() => import('./SendFox/SendFoxAuthorization')) const TwilioAuthorization = lazy(() => import('./Twilio/TwilioAuthorization')) const MailerLiteAuthorization = lazy(() => import('./MailerLite/MailerLiteAuthorization')) +const InstasentAuthorization = lazy(() => import('./Instasent/InstasentAuthorization')) const VboutAuthorization = lazy(() => import('./Vbout/VboutAuthorization')) const FreshdeskAuthorization = lazy(() => import('./Freshdesk/FreshdeskAuthorization')) const GoogleContactsAuthorization = lazy(() => import('./GoogleContacts/GoogleContactsAuthorization')) @@ -430,6 +431,8 @@ export default function IntegInfo() { return case 'MailerLite': return + case 'Instasent': + return case 'Vbout': return case 'Freshdesk': diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 842ed6ac0..b6d931852 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -70,6 +70,7 @@ const Acumbamail = lazy(() => import('./Acumbamail/Acumbamail')) const Groundhogg = lazy(() => import('./Groundhogg/Groundhogg')) const SendFox = lazy(() => import('./SendFox/SendFox')) const MailerLite = lazy(() => import('./MailerLite/MailerLite')) +const Instasent = lazy(() => import('./Instasent/Instasent')) const Vbout = lazy(() => import('./Vbout/Vbout')) const WhatsApp = lazy(() => import('./WhatsApp/WhatsApp')) const LearnDesh = lazy(() => import('./LearnDash/LearnDash')) @@ -736,6 +737,15 @@ export default function NewInteg({ allIntegURL }) { setFlow={setFlow} /> ) + case 'Instasent': + return ( + + ) case 'Vbout': return ( |L$b5lt1OiF>gRc+~se>cicH8E?j||D7viH9>x5`;U_?^J}$O{r`Vb&aQHzNFwaL*E~?&1VPH@5itP>|C4>9N$#zwIdOBkH%g@{ z&@6<2$0v{maCSKXf+BHynJ9Auoi4cukS0EmV(Ls-v{V2j9GlXOkRdn)wrOesF+1_N zB%;w8OlStm10d7wWFSD&o$3iX;^WCnom<>hHJv&$-UBIP zE!z205r__tFJDf`!w0nT{j0s(;U0xyP*aI-03E!B0#F?6<`C4D1a3w_q;~GWfvL{X zz(XzLDGt~^_;#_g@dKu+I+6MC^e6vv?B7A$M5v^@xeI>LTmZ=DrnuW%ohr6jqq8#= zz?4t_dHBTV-=>4xCIM7uD==j>oc?V0rB0>Nc{{66YRj3^%hS%@Pn=E-HzsnryQayE zY55U{ztw;KxBm22DiF;>)nsZ~0BThD56rg`mUnY=@dd;ZzPJU0f!k0dXe3+Jt$sV? zKda@`Mcx8}2<{93wL}|R4k3u*CNR(fLtwew?DFOC7t@zxdWTz5Q$XUuumDpf8G-@C zmJ|#$V-CwQ2f}XvKs12uCea1GBrsk}`vLouDjFne3lRcbXh=E5uAa+HytFiM5xB+@ z#Y;iX0&Qsy?1p7lZ4|bZT;9C4-BUkw^*e4;6=(Szv6if+sVo>3m&%x|OJ5~nb3t&a zRir+Lgce(nd`%sP?l7NQcY^{d8uHT=a<}iS;C=tCw^C7gVYN>m5-hUk5@d8WYn#J%bt1d}-QBaCsxi&`N9j6k`bc zrTGe7j@*5~H244k1!HJTACG`pj$J!W;zq3Amgd=FCeWV$Z+OaG=g(L(VtCR#&3dnE z8V|->2EP)Bk(!GO8&mSCVhHp^@|3WYO2jn)=DqB+4DU5p3P#n#Tt0S!`XtSl2|L!)h3cEu%`sF0^q*uysqZ)cF-K0a}HwhM7PF z%FktwyOdOmfZeC=I9~ibr#Jq5^cc9t#~J(K8kr!h57Ar{RlN_mSK(#pMZ;8FG?_zM zquH&?!xfanxQYyPH)|tH!kzoE+wE-096h}C^Z%YdCBTZHDIh&h;35-aPsyLf?y&L18T$d-YTvZNUb3QRCAc7 zcF>2@`5^$pO&|l*w4ye@f^-O6gBZ+fgP}rhp#)L)rdh~Qpc$H0tP}}2R;Lt}*%3pY zf9uoJ+paq9-7n{~-}xVS!-fPUa1AZey@J7XIlhY=W1;R$C?0d3-2&XT0TAk3yR<@M z7oUob3Q0}#&Z+=$3!??KdKCasFd68=wm!~vx94>ehEMqV=IDOezGFLQSL zc)F~Tj5IoYtLhf)Isi4xerrHTby-5m@}S$10+Za=&>@q_9NLC~a%l|~bnoirQknXO zL^EvZQl6QyRuo_`r?8O$a;)_d^1J!l&;G699>Y8)#8nU>0_NC?K~-F%8Z5=Wx2e(y zaT|(JN~pC=m1R-o3qR(UDAzTMQB~Gb!1*D=t-HvFeKeixIEsQqTtg z!QgE(SmHb@1im8mGIQ7ijDf|s1C}E`e&l$0Jung78K6Z!#(QX&w(C-|x#*Q>iEIV1 z3O=uK!EPW<(hUIo{JQ_APve(YL2Zs^VKj?c98oLW z8*Y12Zht&g;lVYJ9}fAL%jen2!{1E-m2Bv5c(mQl~Ip#zWn&pCmul!bt=}cqhZU_PrZLX zgg6tAyZqc8-u)jCg1bDX8_>AH^6dK|rP%TmOilMz#eAvn2mD_JV_)V)9ahR_?_e8< zr4|vxn|H8!sdwhntl!J~54t#jd>F?se~z_&y1d;Rww;$&RU0fO;?7x$!7`p5=DoFA zKHDlSK<68^)?AorLIjKg1krgrIv{{po(M&J0hk6mjmv5klwZVU;hj@6akd&z&bDHa6w=LD4A{tfmCS0&`N1Qu!u{kf5ThA zOjEl9M+c{KWDp3Mh%HSfkdOmF2%+cspO=L7@-n}%Jn;^QKXjpxGCDSu83RBGFD<4J z{pUN}aw@wnpW)o@|D_>_25^md9RaxjAc%FsfjRNsfgg3fe;*coQ zO>{H_Z+AyV8cDgg>J;Gw1#=J-MLMD8@=R>m%8&kY*B|}v5R68Hn$hJF*dRj@91ekQ zqDw7dTR$ru_Dj*-kRSoFng~L0fucbqWCBP56k&ow<~BYCTGEU=jur!&FRjqw5K}Y? zHzXipYt0vSp+B5(w`5=h1c$4l2`L6Zc-UHU?DNs@fiMe%d~pJT43FRZ%+wur&$vsP zgrICgjw=ov=M2vtR}~il`Wa@fGkR78gTW@$J>-eMJ zcm31Ju{~qV>7ugC;*NMNeRK_dUW9FFupqA|)Q0p1u9RB7%W5W3jy!fA1ANe0Wy*}aB8cM^9g2qGu~6vZpV z7p!V9$c(JtC1?vG?hoaEe@ZzbI_}fF6f@8Wf*@q#9{_^PEe4Wuv?grL7>%X=z|aAX z`*baCP=G`dpiG4LLKK@>ie^x$j?J^BaQ?RzagS<+G!j6jOjs$?i5%kcVaZ|BLOpA9+WS}1f z33^?+`Q)C#+pzh03C|b~tgZzMUS+yK-KhE^nv}CmOOfz8hryR!?58SSCDhVN=t*CC(fEuWxWk`WeqZ=T@(C+zC z9jMK0AFhA@ZPj1u`JNV@J!D~w1lU@noxsY-CPBFkL#SW?2|YR&=x2W$pZcq(6*9oz z3qG$&vY}=&;s5{u8RaEqzyG%7LBh4Hkq61vgX1^1Fx}~D(0{_>kj-oz KG9Z0m0001XVXxT$ literal 0 HcmV?d00001 From 33d5027bef6660acaa8be7d87c04217f411206ce Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 18 Jun 2026 16:34:28 +0600 Subject: [PATCH 2/4] fix(instasent): address code-review findings - 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
--- backend/Actions/Instasent/RecordApiHelper.php | 2 +- .../src/Utils/StaticData/tutorialLinks.js | 3 + .../Instasent/InstasentAuthorization.jsx | 2 - .../Instasent/InstasentCommonFunc.js | 18 +++++- .../Instasent/InstasentFieldMap.jsx | 64 +++++++++++++------ .../Instasent/InstasentIntegLayout.jsx | 51 ++++++++------- .../Instasent/IntegrationHelpers.jsx | 3 - 7 files changed, 94 insertions(+), 49 deletions(-) diff --git a/backend/Actions/Instasent/RecordApiHelper.php b/backend/Actions/Instasent/RecordApiHelper.php index 08d516094..b9636697c 100644 --- a/backend/Actions/Instasent/RecordApiHelper.php +++ b/backend/Actions/Instasent/RecordApiHelper.php @@ -43,7 +43,7 @@ public function generateReqDataFromFieldMap($data, $fieldMap) if ($triggerValue === 'custom' && isset($value->customValue)) { $dataFinal[$actionValue] = Common::replaceFieldWithValue($value->customValue, $data); - } elseif (!\is_null($data[$triggerValue])) { + } elseif (isset($data[$triggerValue])) { $dataFinal[$actionValue] = $data[$triggerValue]; } } diff --git a/frontend/src/Utils/StaticData/tutorialLinks.js b/frontend/src/Utils/StaticData/tutorialLinks.js index ca9524cd4..b95d5852a 100644 --- a/frontend/src/Utils/StaticData/tutorialLinks.js +++ b/frontend/src/Utils/StaticData/tutorialLinks.js @@ -225,6 +225,9 @@ const tutorialLinks = { youTubeLink: 'https://www.youtube.com/playlist?list=PL7c6CDwwm-AKe60pZMUmlWnHQWKrx8nw8', docLink: 'https://bit-integrations.com/wp-docs/actions/mailerlite-integrations/' }, + instasent: { + docLink: '#' + }, mailchimp: { youTubeLink: 'https://www.youtube.com/playlist?list=PL7c6CDwwm-ALUaeqiK9GwBSxVkAod1PzP', docLink: 'https://bit-integrations.com/wp-docs/actions/mailchimp-integrations/' diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx index 2e7e16e93..3e1b99749 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentAuthorization.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-expressions */ import { useState } from 'react' import { __ } from '../../../Utils/i18nwrap' import LoaderSm from '../../Loaders/LoaderSm' @@ -25,7 +24,6 @@ export default function InstasentAuthorization({ document.getElementById('btcd-settings-wrp').scrollTop = 0 }, 300) - !instasentConf?.default setstep(2) } diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js index 1890104cd..1c1b7e5c4 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js @@ -19,6 +19,13 @@ export const ACTIONS_WITH_DATASOURCE = [ 'create_contact_event' ] +// Actions whose field map may carry arbitrary keys (contact custom attributes / +// event parameters), entered via the "Custom Field..." option in the field map. +export const ACTIONS_WITH_CUSTOM_FIELDS = ['create_or_update_contact', 'create_contact_event'] + +// Sentinel chosen in the Instasent-field select to switch a row to a free-text key. +export const CUSTOM_FIELD_KEY = '__custom_field__' + export const InstasentStaticData = { send_sms: [ { key: 'from', label: 'From', required: true }, @@ -55,6 +62,14 @@ export const handleInput = (e, instasentConf, setInstasentConf, loading, setLoad const updatedConf = create(instasentConf, draftConf => { draftConf[name] = value + + // Changing the project invalidates the fetched data sources + the selection. + if (name === 'projectId') { + draftConf.datasourceId = '' + if (draftConf.default) { + draftConf.default.datasources = [] + } + } }) setInstasentConf(updatedConf) @@ -88,7 +103,8 @@ export const checkMappedFields = instasentConf => { mappedField => !mappedField.formField || !mappedField.instasentFormField || - (!mappedField.formField === 'custom' && !mappedField.customValue) + mappedField.instasentFormField === CUSTOM_FIELD_KEY || + (mappedField.formField === 'custom' && !mappedField.customValue) ) : [] if (mappedFields.length > 0) { diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx index 5cd98282d..99f6f1f02 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx @@ -5,6 +5,7 @@ import { __, sprintf } from '../../../Utils/i18nwrap' import { handleCustomValue } from '../IntegrationHelpers/IntegrationHelpers' import TagifyInput from '../../Utilities/TagifyInput' import { addFieldMap, delFieldMap, handleFieldMapping } from './IntegrationHelpers' +import { ACTIONS_WITH_CUSTOM_FIELDS, CUSTOM_FIELD_KEY } from './InstasentCommonFunc' export default function InstasentFieldMap({ i, formFields, field, instasentConf, setInstasentConf }) { const btcbi = useRecoilValue($appConfigState) @@ -13,6 +14,17 @@ export default function InstasentFieldMap({ i, formFields, field, instasentConf, const requiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === true) || [] const nonRequiredFlds = instasentConf?.instasentFields.filter(fld => fld.required === false) || [] + const isExtraRow = i >= requiredFlds.length + const customCapable = ACTIONS_WITH_CUSTOM_FIELDS.includes(instasentConf?.action) + const predefinedKeys = (instasentConf?.instasentFields || []).map(fld => fld.key) + const isCustomKey = + isExtraRow && + customCapable && + !!field.instasentFormField && + field.instasentFormField !== CUSTOM_FIELD_KEY && + !predefinedKeys.includes(field.instasentFormField) + const showCustomKeyInput = field.instasentFormField === CUSTOM_FIELD_KEY || isCustomKey + return (
@@ -57,25 +69,41 @@ export default function InstasentFieldMap({ i, formFields, field, instasentConf, /> )} - handleFieldMapping(ev, i, instasentConf, setInstasentConf)} + /> + ) : ( + + ) : ( + <> + {nonRequiredFlds.map(({ key, label }) => ( + + ))} + {customCapable && ( + + )} + + )} + + )}
{i >= requiredFlds.length && ( <> diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx index 67f3b5e99..efe0d041c 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx @@ -113,31 +113,34 @@ export default function InstasentIntegLayout({ />
)} -
+ {ACTIONS_WITH_DATASOURCE.includes(instasentConf?.action) && ( -
- {__('Data Source:', 'bit-integrations')} - setDatasource(val)} - options={(instasentConf?.default?.datasources || []).map(ds => ({ - label: ds.name || ds.id, - value: String(ds.id) - }))} - singleSelect - closeOnSelect - /> - -
+ <> +
+
+ {__('Data Source:', 'bit-integrations')} + setDatasource(val)} + options={(instasentConf?.default?.datasources || []).map(ds => ({ + label: ds.name || ds.id, + value: String(ds.id) + }))} + singleSelect + closeOnSelect + /> + +
+ )} {loading.field && ( diff --git a/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx index b9bbc114e..9a0438351 100644 --- a/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/IntegrationHelpers.jsx @@ -1,6 +1,3 @@ -/* eslint-disable no-unused-expressions */ -import { __ } from '../../../Utils/i18nwrap' - export const addFieldMap = (i, confTmp, setConf) => { const newConf = { ...confTmp } newConf.field_map.splice(i, 0, {}) From 9d1675b49643ac7612867601a462421907f5bf33 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 18 Jun 2026 16:50:19 +0600 Subject: [PATCH 3/4] fix(instasent): apply PR review suggestions - 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 --- .../Actions/Instasent/InstasentController.php | 16 ++++++++++------ backend/Actions/Instasent/RecordApiHelper.php | 11 +++++++++++ .../Instasent/EditInstasent.jsx | 18 ++++++++++-------- .../Instasent/InstasentFieldMap.jsx | 4 ++-- .../Instasent/InstasentIntegLayout.jsx | 4 ++-- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/backend/Actions/Instasent/InstasentController.php b/backend/Actions/Instasent/InstasentController.php index 4de0d4cd1..293f49401 100644 --- a/backend/Actions/Instasent/InstasentController.php +++ b/backend/Actions/Instasent/InstasentController.php @@ -18,7 +18,7 @@ class InstasentController public function authorize($refreshFieldsRequestParams) { - if (empty($refreshFieldsRequestParams->auth_token)) { + if (empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token)) { wp_send_json_error( __( 'Requested parameter is empty', @@ -37,6 +37,10 @@ public function authorize($refreshFieldsRequestParams) $response = HttpHelper::get($endpoint, null, $header); + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + if (HttpHelper::$responseCode == 200) { wp_send_json_success('Authorization Successful', 200); @@ -51,7 +55,7 @@ public function authorize($refreshFieldsRequestParams) public function refreshDatasources($refreshFieldsRequestParams) { - if (empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) { + if (empty($refreshFieldsRequestParams) || empty($refreshFieldsRequestParams->auth_token) || empty($refreshFieldsRequestParams->projectId)) { wp_send_json_error( __('Requested parameter is empty', 'bit-integrations'), 400 @@ -68,6 +72,10 @@ public function refreshDatasources($refreshFieldsRequestParams) $response = HttpHelper::get($endpoint, null, $header); + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + if (HttpHelper::$responseCode == 200) { $entities = $response->entities ?? []; $formattedResponse = []; @@ -115,10 +123,6 @@ public function execute($integrationData, $fieldValues) $action ); - if (is_wp_error($instasentApiResponse)) { - return $instasentApiResponse; - } - return $instasentApiResponse; } } diff --git a/backend/Actions/Instasent/RecordApiHelper.php b/backend/Actions/Instasent/RecordApiHelper.php index b9636697c..fe1bc233b 100644 --- a/backend/Actions/Instasent/RecordApiHelper.php +++ b/backend/Actions/Instasent/RecordApiHelper.php @@ -109,6 +109,17 @@ public function execute( break; } + 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; if ( diff --git a/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx index 326ae6c13..f5b2cedbc 100644 --- a/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/EditInstasent.jsx @@ -10,7 +10,7 @@ import SnackMsg from '../../Utilities/SnackMsg' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' -import { checkMappedFields, handleInput } from './InstasentCommonFunc' +import { checkMappedFields, handleInput, InstasentStaticData } from './InstasentCommonFunc' import InstasentIntegLayout from './InstasentIntegLayout' function EditInstasent({ allIntegURL }) { @@ -46,13 +46,15 @@ function EditInstasent({ allIntegURL }) { } useEffect(() => { - if (!instasentConf?.action) { - setInstasentConf(prev => - create(prev, draftConf => { - draftConf.action = 'send_sms' - }) - ) - } + setInstasentConf(prev => + create(prev, draftConf => { + const action = draftConf.action || 'send_sms' + draftConf.action = action + // instasentFields isn't persisted; rebuild it from the saved action so the + // field map can render its options on edit (without touching the saved field_map). + draftConf.instasentFields = InstasentStaticData[action] || [] + }) + ) }, []) const handleEditIntegName = e => { diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx index 99f6f1f02..958061df9 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentFieldMap.jsx @@ -11,8 +11,8 @@ export default function InstasentFieldMap({ i, formFields, field, instasentConf, const btcbi = useRecoilValue($appConfigState) const { isPro } = btcbi - 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) || [] const isExtraRow = i >= requiredFlds.length const customCapable = ACTIONS_WITH_CUSTOM_FIELDS.includes(instasentConf?.action) diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx index efe0d041c..56653a245 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx @@ -125,8 +125,8 @@ export default function InstasentIntegLayout({ className="w-5 d-in-b" onChange={val => setDatasource(val)} options={(instasentConf?.default?.datasources || []).map(ds => ({ - label: ds.name || ds.id, - value: String(ds.id) + label: ds?.name || ds?.id || '', + value: String(ds?.id || '') }))} singleSelect closeOnSelect From 8b5c5575f330b18676b4bae93edd254b8a95081e Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 23 Jun 2026 13:04:06 +0600 Subject: [PATCH 4/4] refactor(instasent): move allowUnicode from field map to Utilities checkbox 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. --- .../Actions/Instasent/InstasentController.php | 4 ++- backend/Actions/Instasent/RecordApiHelper.php | 7 ++++- .../AllIntegrations/Instasent/Instasent.jsx | 1 + .../Instasent/InstasentActions.jsx | 31 +++++++++++++++++++ .../Instasent/InstasentCommonFunc.js | 3 +- .../Instasent/InstasentIntegLayout.jsx | 14 +++++++++ 6 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx diff --git a/backend/Actions/Instasent/InstasentController.php b/backend/Actions/Instasent/InstasentController.php index 293f49401..3317f1e03 100644 --- a/backend/Actions/Instasent/InstasentController.php +++ b/backend/Actions/Instasent/InstasentController.php @@ -104,6 +104,7 @@ public function execute($integrationData, $fieldValues) $auth_token = $integrationDetails->auth_token ?? ''; $fieldMap = $integrationDetails->field_map ?? ''; $action = $integrationDetails->action ?? ''; + $actions = isset($integrationDetails->actions) ? $integrationDetails->actions : (object) []; if ( empty($fieldMap) @@ -120,7 +121,8 @@ public function execute($integrationData, $fieldValues) $fieldValues, $fieldMap, $auth_token, - $action + $action, + $actions ); return $instasentApiResponse; diff --git a/backend/Actions/Instasent/RecordApiHelper.php b/backend/Actions/Instasent/RecordApiHelper.php index fe1bc233b..bdfdf3f08 100644 --- a/backend/Actions/Instasent/RecordApiHelper.php +++ b/backend/Actions/Instasent/RecordApiHelper.php @@ -56,10 +56,15 @@ public function execute( $fieldValues, $fieldMap, $auth_token, - $action + $action, + $utilities = null ) { $fieldData = $this->generateReqDataFromFieldMap($fieldValues, $fieldMap); + if ($action === 'send_sms' && isset($utilities->allowUnicode)) { + $fieldData['allowUnicode'] = $utilities->allowUnicode; + } + $default = [ 'success' => false, 'message' => __('Bit Integrations Pro is required.', 'bit-integrations'), diff --git a/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx index c915a4f85..2e0be1b11 100644 --- a/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/Instasent.jsx @@ -34,6 +34,7 @@ function Instasent({ formFields, setFlow, flow, allIntegURL }) { datasourceId: '', instasentFields: [], field_map: [{ formField: '', instasentFormField: '' }], + actions: {}, default: { datasources: [] } }) diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx new file mode 100644 index 000000000..6cfe19c73 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentActions.jsx @@ -0,0 +1,31 @@ +import { __ } from '../../../Utils/i18nwrap' +import TableCheckBox from '../../Utilities/TableCheckBox' + +export default function InstasentActions({ instasentConf, setInstasentConf }) { + const actionHandler = (e, type) => { + const newConf = { ...instasentConf, actions: { ...instasentConf.actions } } + if (e.target.checked) { + newConf.actions[type] = true + } else { + delete newConf.actions[type] + } + + setInstasentConf({ ...newConf }) + } + + return ( +
+ actionHandler(e, 'allowUnicode')} + className="wdt-200 mt-4 mr-2" + value="allow_unicode" + title={__('Allow Unicode', 'bit-integrations')} + subTitle={__( + 'Enable Unicode support for SMS messages (e.g. emojis, special characters)', + 'bit-integrations' + )} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js index 1c1b7e5c4..90b7e16a8 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentCommonFunc.js @@ -31,8 +31,7 @@ export const InstasentStaticData = { { key: 'from', label: 'From', required: true }, { key: 'to', label: 'To', required: true }, { key: 'text', label: 'Text', required: true }, - { key: 'clientId', label: 'Client Id', required: false }, - { key: 'allowUnicode', label: 'Allow Unicode', required: false } + { key: 'clientId', label: 'Client Id', required: false } ], create_lookup: [{ key: 'to', label: 'To', required: true }], create_datasource: [ diff --git a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx index 56653a245..7330a7486 100644 --- a/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Instasent/InstasentIntegLayout.jsx @@ -12,6 +12,7 @@ import { instasentRefreshFields, refreshDatasources } from './InstasentCommonFunc' +import InstasentActions from './InstasentActions' import InstasentFieldMap from './InstasentFieldMap' const actionOptions = [ @@ -206,6 +207,19 @@ export default function InstasentIntegLayout({


+ + {instasentConf?.action === 'send_sms' && ( + <> +
+ {__('Utilities', 'bit-integrations')} +
+
+ + + )} )}