From 89d583452d4b8b0bd587330935181e835223e34e Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 11 Jun 2026 16:16:32 +0600 Subject: [PATCH 1/4] feat: sender integration added --- backend/Actions/Sender/RecordApiHelper.php | 94 +++++++++ backend/Actions/Sender/Routes.php | 12 ++ backend/Actions/Sender/SenderController.php | 137 ++++++++++++++ .../src/Utils/StaticData/tutorialLinks.js | 4 + .../components/AllIntegrations/EditInteg.jsx | 3 + .../components/AllIntegrations/IntegInfo.jsx | 3 + .../components/AllIntegrations/NewInteg.jsx | 10 + .../AllIntegrations/Sender/EditSender.jsx | 75 ++++++++ .../AllIntegrations/Sender/Sender.jsx | 124 ++++++++++++ .../AllIntegrations/Sender/SenderActions.jsx | 129 +++++++++++++ .../Sender/SenderAuthorization.jsx | 112 +++++++++++ .../Sender/SenderCommonFunc.js | 105 +++++++++++ .../AllIntegrations/Sender/SenderFieldMap.jsx | 108 +++++++++++ .../Sender/SenderIntegLayout.jsx | 178 ++++++++++++++++++ .../AllIntegrations/Sender/staticData.js | 95 ++++++++++ .../src/components/Flow/New/SelectAction.jsx | 1 + frontend/src/resource/img/integ/sender.webp | Bin 0 -> 5434 bytes 17 files changed, 1190 insertions(+) create mode 100644 backend/Actions/Sender/RecordApiHelper.php create mode 100644 backend/Actions/Sender/Routes.php create mode 100644 backend/Actions/Sender/SenderController.php create mode 100644 frontend/src/components/AllIntegrations/Sender/EditSender.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/Sender.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/SenderActions.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/Sender/SenderFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/Sender/staticData.js create mode 100644 frontend/src/resource/img/integ/sender.webp diff --git a/backend/Actions/Sender/RecordApiHelper.php b/backend/Actions/Sender/RecordApiHelper.php new file mode 100644 index 000000000..06606790f --- /dev/null +++ b/backend/Actions/Sender/RecordApiHelper.php @@ -0,0 +1,94 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function execute($fieldValues, $fieldMap) + { + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + $mainAction = $this->_integrationDetails->mainAction ?? ''; + + $defaultResponse = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'), + ]; + + $actionHookMap = [ + 'create_or_update_subscriber' => 'sender_create_or_update_subscriber', + 'update_subscriber' => 'sender_update_subscriber', + 'delete_subscriber' => 'sender_delete_subscriber', + 'remove_phone_from_subscriber' => 'sender_remove_phone_from_subscriber', + 'add_subscriber_to_group' => 'sender_add_subscriber_to_group', + 'remove_subscriber_from_group' => 'sender_remove_subscriber_from_group', + 'create_group' => 'sender_create_group', + 'update_group' => 'sender_update_group', + 'delete_group' => 'sender_delete_group', + ]; + + if (!isset($actionHookMap[$mainAction])) { + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations'), + ]; + + LogHandler::save($this->_integrationID, ['type' => 'Sender', 'type_name' => 'unknown'], 'error', $response); + + return $response; + } + + $response = Hooks::apply( + Config::withPrefix($actionHookMap[$mainAction]), + $defaultResponse, + $fieldData, + $this->_integrationDetails + ); + + $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + LogHandler::save($this->_integrationID, ['type' => 'Sender', 'type_name' => $mainAction], $responseType, $response); + + return $response; + } + + private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $dataFinal = []; + foreach ($fieldMap as $item) { + if (empty($item->senderField)) { + continue; + } + + $triggerValue = $item->formField; + $actionValue = $item->senderField; + + $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) + ? Common::replaceFieldWithValue($item->customValue, $fieldValues) + : ($fieldValues[$triggerValue] ?? ''); + } + + return $dataFinal; + } +} diff --git a/backend/Actions/Sender/Routes.php b/backend/Actions/Sender/Routes.php new file mode 100644 index 000000000..96668d46e --- /dev/null +++ b/backend/Actions/Sender/Routes.php @@ -0,0 +1,12 @@ +api_token)) { + wp_send_json_error(__('API token is required', 'bit-integrations'), 400); + } + + HttpHelper::get(self::$baseUrl . '/groups', null, self::authHeader($requestParams->api_token)); + + if (HttpHelper::$responseCode >= 200 && HttpHelper::$responseCode < 300) { + wp_send_json_success(__('Authorized Successfully', 'bit-integrations'), 200); + } + + wp_send_json_error(__('Invalid API token', 'bit-integrations'), 400); + } + + /** + * Fetch the account groups for the group dropdowns. + * + * @param object $requestParams + */ + public static function refreshGroups($requestParams) + { + if (empty($requestParams->api_token)) { + wp_send_json_error(__('API token is required', 'bit-integrations'), 400); + } + + $response = HttpHelper::get(self::$baseUrl . '/groups', null, self::authHeader($requestParams->api_token)); + + if (HttpHelper::$responseCode < 200 || HttpHelper::$responseCode >= 300) { + wp_send_json_error(__('Failed to fetch groups', 'bit-integrations'), 400); + } + + $groups = []; + foreach (self::dataRows($response) as $group) { + $groups[] = [ + 'id' => $group->id ?? '', + 'title' => $group->title ?? ($group->name ?? ''), + ]; + } + + wp_send_json_success(['groups' => $groups], 200); + } + + /** + * Fetch the account custom fields so they can be mapped on subscriber actions. + * + * @param object $requestParams + */ + public static function refreshFields($requestParams) + { + if (empty($requestParams->api_token)) { + wp_send_json_error(__('API token is required', 'bit-integrations'), 400); + } + + $response = HttpHelper::get(self::$baseUrl . '/fields', null, self::authHeader($requestParams->api_token)); + + if (HttpHelper::$responseCode < 200 || HttpHelper::$responseCode >= 300) { + wp_send_json_error(__('Failed to fetch fields', 'bit-integrations'), 400); + } + + $fields = []; + foreach (self::dataRows($response) as $field) { + $key = $field->id ?? ''; + if ($key === '') { + continue; + } + + $fields[] = [ + 'key' => (string) $key, + 'label' => $field->name ?? ($field->title ?? (string) $key), + ]; + } + + wp_send_json_success(['fields' => $fields], 200); + } + + public function execute($integrationData, $fieldValues) + { + $integrationDetails = $integrationData->flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map ?? []; + + if (empty($integrationDetails->api_token)) { + return new WP_Error('api_token_empty', __('Sender API token is required', 'bit-integrations')); + } + + $recordApiHelper = new RecordApiHelper($integrationDetails, $integId); + $senderResponse = $recordApiHelper->execute($fieldValues, $fieldMap); + + if (is_wp_error($senderResponse)) { + return $senderResponse; + } + + return $senderResponse; + } + + private static function authHeader($token) + { + return [ + 'Authorization' => 'Bearer ' . $token, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]; + } + + private static function dataRows($response) + { + if (isset($response->data) && \is_array($response->data)) { + return $response->data; + } + + return \is_array($response) ? $response : []; + } +} diff --git a/frontend/src/Utils/StaticData/tutorialLinks.js b/frontend/src/Utils/StaticData/tutorialLinks.js index ca9524cd4..434b54b9a 100644 --- a/frontend/src/Utils/StaticData/tutorialLinks.js +++ b/frontend/src/Utils/StaticData/tutorialLinks.js @@ -674,6 +674,10 @@ const tutorialLinks = { bookingPress: { youTubeLink: '', docLink: '' + }, + sender: { + youTubeLink: '', + docLink: 'https://bit-integrations.com/wp-docs/actions/sender-integrations/' } } export default tutorialLinks diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index baf8e2a17..5f2751922 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -26,6 +26,7 @@ const EditPod = lazy(() => import('./Pods/EditPod')) const EditMailPoet = lazy(() => import('./MailPoet/EditMailPoet')) const EditMailerPress = lazy(() => import('./MailerPress/EditMailerPress')) const EditSendinBlue = lazy(() => import('./SendinBlue/EditSendinBlue')) +const EditSender = lazy(() => import('./Sender/EditSender')) const EditWooCommerce = lazy(() => import('./WooCommerce/EditWooCommerce')) const EditActiveCampaign = lazy(() => import('./ActiveCampaign/EditActiveCampaign')) const EditWebHooks = lazy(() => import('./WebHooks/EditWebHooks')) @@ -305,6 +306,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'MailerPress': return + case 'Sender': + return case 'SendinBlue': case 'Brevo(Sendinblue)': return diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index c095b228d..0c69a78c1 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -32,6 +32,7 @@ const MailChimpAuthorization = lazy(() => import('./MailChimp/MailChimpAuthoriza const MailPoetAuthorization = lazy(() => import('./MailPoet/MailPoetAuthorization')) const MailerPressAuthorization = lazy(() => import('./MailerPress/MailerPressAuthorization')) const SendinblueAuthorization = lazy(() => import('./SendinBlue/SendinBlueAuthorization')) +const SenderAuthorization = lazy(() => import('./Sender/SenderAuthorization')) const WooCommerceAuthorization = lazy(() => import('./WooCommerce/WooCommerceAuthorization')) const ActiveCampaignAuthorization = lazy(() => import('./ActiveCampaign/ActiveCampaignAuthorization')) const ZohoFlowAuthorization = lazy(() => import('./IntegrationHelpers/WebHook/WebHooksIntegration')) @@ -340,6 +341,8 @@ export default function IntegInfo() { return case 'MailerPress': return + case 'Sender': + return case 'SendinBlue': case 'Brevo(Sendinblue)': return diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 842ed6ac0..b761c4db7 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -27,6 +27,7 @@ const MailChimp = lazy(() => import('./MailChimp/MailChimp')) const MailPoet = lazy(() => import('./MailPoet/MailPoet')) const MailerPress = lazy(() => import('./MailerPress/MailerPress')) const Sendinblue = lazy(() => import('./SendinBlue/SendinBlue')) +const Sender = lazy(() => import('./Sender/Sender')) const WooCommerce = lazy(() => import('./WooCommerce/WooCommerce')) const Pods = lazy(() => import('./Pods/Pods')) const ActiveCampaign = lazy(() => import('./ActiveCampaign/ActiveCampaign')) @@ -331,6 +332,15 @@ export default function NewInteg({ allIntegURL }) { setFlow={setFlow} /> ) + case 'Sender': + return ( + + ) case 'SendinBlue': return ( + + +
+ {__('Integration Name:', 'bit-integrations')} + handleInput(e, senderConf, setSenderConf)} + name="name" + value={senderConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: senderConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar + }) + } + disabled={!checkMappedFields(senderConf)} + isLoading={isLoading} + dataConf={senderConf} + setDataConf={setSenderConf} + formFields={formFields} + /> +
+ + ) +} diff --git a/frontend/src/components/AllIntegrations/Sender/Sender.jsx b/frontend/src/components/AllIntegrations/Sender/Sender.jsx new file mode 100644 index 000000000..0c1a94a27 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/Sender.jsx @@ -0,0 +1,124 @@ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { useNavigate } from 'react-router' +import toast from 'react-hot-toast' +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 SenderAuthorization from './SenderAuthorization' +import { checkMappedFields } from './SenderCommonFunc' +import SenderIntegLayout from './SenderIntegLayout' + +function Sender({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(false) + const [loading, setLoading] = useState({ field: false, auth: false, group: false }) + const [step, setstep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + + const [senderConf, setSenderConf] = useState({ + name: 'Sender', + type: 'Sender', + api_token: '', + field_map: [{ formField: '', senderField: '' }], + mainAction: '', + senderFields: [], + allGroups: [], + allFields: [], + groups: [], + groupId: '', + actions: {} + }) + + const saveConfig = () => { + if (!checkMappedFields(senderConf)) { + toast.error(__('Please map mandatory fields', 'bit-integrations')) + return + } + + setIsLoading(true) + const resp = saveIntegConfig(flow, setFlow, allIntegURL, senderConf, 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 (!senderConf.mainAction) { + toast.error(__('Please select an action', 'bit-integrations')) + return + } + if (!checkMappedFields(senderConf)) { + toast.error(__('Please map mandatory fields', 'bit-integrations')) + return + } + setstep(pageNo) + } + + return ( +
+ +
+ +
+ + {/* STEP 1 */} + + + {/* STEP 2 */} +
+ + + {senderConf?.mainAction && ( + + )} +
+ + {/* STEP 3 */} + saveConfig()} + isLoading={isLoading} + dataConf={senderConf} + setDataConf={setSenderConf} + formFields={formFields} + /> +
+ ) +} + +export default Sender diff --git a/frontend/src/components/AllIntegrations/Sender/SenderActions.jsx b/frontend/src/components/AllIntegrations/Sender/SenderActions.jsx new file mode 100644 index 000000000..70cb97738 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/SenderActions.jsx @@ -0,0 +1,129 @@ +/* eslint-disable no-param-reassign */ + +import { create } from 'mutative' +import { useState } from 'react' +import MultiSelect from 'react-multiple-select-dropdown-lite' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { __ } from '../../../Utils/i18nwrap' +import Loader from '../../Loaders/Loader' +import ConfirmModal from '../../Utilities/ConfirmModal' +import TableCheckBox from '../../Utilities/TableCheckBox' +import { refreshSenderGroups } from './SenderCommonFunc' +import { subscriberActions } from './staticData' + +export default function SenderActions({ senderConf, setSenderConf, isLoading, setIsLoading }) { + const [actionMdl, setActionMdl] = useState({ show: false }) + + const clsActionMdl = () => setActionMdl({ show: false }) + + const actionHandler = (e, type) => { + if (type === 'groups') { + refreshSenderGroups(senderConf, setSenderConf, setIsLoading) + } + + setActionMdl({ show: type }) + + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf.actions = { ...(draftConf.actions || {}) } + + if (e.target.checked) { + draftConf.actions[type] = true + } else { + delete draftConf.actions[type] + if (type === 'groups') { + draftConf.groups = [] + } + } + }) + ) + } + + const setGroups = val => { + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf.actions = { ...(draftConf.actions || {}) } + draftConf.groups = val + + if (val.length) { + draftConf.actions.groups = true + } else { + delete draftConf.actions.groups + } + }) + ) + } + + const groupOptions = (senderConf?.allGroups ?? []).map(group => ({ + label: group.title, + value: String(group.id) + })) + + return ( + <> +
+ {subscriberActions.includes(senderConf?.mainAction) && ( + actionHandler(e, 'groups')} + className="wdt-200 mt-4 mr-2" + value="groups" + title={__('Groups', 'bit-integrations')} + subTitle={__('Add the subscriber to groups', 'bit-integrations')} + /> + )} + + actionHandler(e, 'trigger_automation')} + className="wdt-200 mt-4 mr-2" + value="trigger_automation" + title={__('Trigger Automation', 'bit-integrations')} + subTitle={__('Trigger automations for the selected groups', 'bit-integrations')} + /> +
+ + +
+ + {isLoading ? ( + + ) : ( +
+ setGroups(val)} + customValue + /> + +
+ )} + + + ) +} diff --git a/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx b/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx new file mode 100644 index 000000000..14e7b2dc7 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx @@ -0,0 +1,112 @@ +/* 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 './SenderCommonFunc' + +export default function SenderAuthorization({ + senderConf, + setSenderConf, + step, + setstep, + loading, + setLoading, + isInfo +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const tokenUrl = 'https://app.sender.net/settings/tokens' + + const nextPage = () => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + setstep(2) + } + + const handleInput = e => { + const newConf = { ...senderConf } + newConf[e.target.name] = e.target.value + setSenderConf(newConf) + } + + const note = ` +

${__('Steps to generate an API access token:', 'bit-integrations')}

+ + ` + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} +
+ + +
+ {__('API Token:', 'bit-integrations')} +
+ + + + {__('To get your API token, please visit', 'bit-integrations')} +   + + {__('Sender API Access Tokens', 'bit-integrations')} + + +
+
+ + {!isInfo && ( +
+ +
+ +
+ )} + +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js b/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js new file mode 100644 index 000000000..38cb9569e --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js @@ -0,0 +1,105 @@ +/* eslint-disable no-else-return */ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' + +export const handleInput = (e, senderConf, setSenderConf) => { + const { name, value } = e.target + + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +export const generateMappedField = fields => { + const requiredFlds = (fields || []).filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ formField: '', senderField: field.key })) + : [{ formField: '', senderField: '' }] +} + +export const checkMappedFields = senderConf => { + const mappedFields = senderConf?.field_map + ? senderConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.senderField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + return mappedFields.length < 1 +} + +export const authorization = (confTmp, setIsAuthorized, loading, setLoading) => { + if (!confTmp.api_token) { + toast.error(__("API token can't be empty", 'bit-integrations')) + return + } + + setLoading({ ...loading, auth: true }) + + bitsFetch({ api_token: confTmp.api_token }, 'sender_authorize').then(result => { + setLoading({ ...loading, auth: false }) + + if (result && result.success) { + setIsAuthorized(true) + toast.success(__('Authorized Successfully', 'bit-integrations')) + return + } + + toast.error(__('Authorization failed', 'bit-integrations')) + }) +} + +export const refreshSenderGroups = (confTmp, setSenderConf, setIsLoading) => { + if (!confTmp.api_token) { + toast.error(__("API token can't be empty", 'bit-integrations')) + return + } + + setIsLoading(true) + bitsFetch({ api_token: confTmp.api_token }, 'refresh_sender_groups') + .then(result => { + if (result && result.success && result.data?.groups) { + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf.allGroups = result.data.groups + }) + ) + setIsLoading(false) + toast.success(__('Groups fetched successfully', 'bit-integrations')) + return + } + setIsLoading(false) + toast.error(__('Groups fetch failed', 'bit-integrations')) + }) + .catch(() => setIsLoading(false)) +} + +export const refreshSenderFields = (confTmp, setSenderConf, setIsLoading) => { + if (!confTmp.api_token) { + toast.error(__("API token can't be empty", 'bit-integrations')) + return + } + + setIsLoading(true) + bitsFetch({ api_token: confTmp.api_token }, 'refresh_sender_fields') + .then(result => { + if (result && result.success && result.data?.fields) { + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf.allFields = result.data.fields + }) + ) + setIsLoading(false) + toast.success(__('Custom fields fetched successfully', 'bit-integrations')) + return + } + setIsLoading(false) + toast.error(__('Custom fields fetch failed', 'bit-integrations')) + }) + .catch(() => setIsLoading(false)) +} diff --git a/frontend/src/components/AllIntegrations/Sender/SenderFieldMap.jsx b/frontend/src/components/AllIntegrations/Sender/SenderFieldMap.jsx new file mode 100644 index 000000000..fd7f11cd3 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/SenderFieldMap.jsx @@ -0,0 +1,108 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping +} from '../GlobalIntegrationHelper' + +export default function SenderFieldMap({ i, formFields, field, senderConf, setSenderConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = senderConf?.senderFields?.filter(fld => fld.required === true) || [] + const staticNonRequired = senderConf?.senderFields?.filter(fld => fld.required === false) || [] + const fetchedFields = Array.isArray(senderConf?.allFields) + ? senderConf.allFields.map(f => ({ key: f.key, label: f.label })) + : [] + const nonRequiredFlds = [...staticNonRequired, ...fetchedFields] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, senderConf, setSenderConf)} + 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/Sender/SenderIntegLayout.jsx b/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx new file mode 100644 index 000000000..28357838f --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx @@ -0,0 +1,178 @@ +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/IntegrationHelpers' +import { generateMappedField, refreshSenderFields, refreshSenderGroups } from './SenderCommonFunc' +import SenderActions from './SenderActions' +import SenderFieldMap from './SenderFieldMap' +import { + modules, + senderFieldsByAction, + singleGroupActions, + subscriberActions, + triggerAutomationActions +} from './staticData' + +export default function SenderIntegLayout({ + formFields, + senderConf, + setSenderConf, + isLoading, + setIsLoading +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const setField = (key, val) => + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf[key] = val + }) + ) + + const handleMainAction = value => { + const fields = senderFieldsByAction[value] || [] + + setSenderConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + draftConf.senderFields = fields + draftConf.field_map = fields.length ? generateMappedField(fields) : [] + }) + ) + + if (singleGroupActions.includes(value) || subscriberActions.includes(value)) { + refreshSenderGroups(senderConf, setSenderConf, setIsLoading) + } + if (subscriberActions.includes(value)) { + refreshSenderFields(senderConf, setSenderConf, setIsLoading) + } + } + + const groupOptions = (senderConf?.allGroups ?? []).map(group => ({ + label: group.title, + value: String(group.id) + })) + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={modules?.map(action => ({ + label: checkIsPro(isPro, action.is_pro) ? action.label : getProLabel(action.label), + value: action.name, + disabled: !checkIsPro(isPro, action.is_pro) + }))} + singleSelect + closeOnSelect + /> +
+ + {singleGroupActions.includes(senderConf?.mainAction) && ( + <> +
+
+ {__('Group:', 'bit-integrations')} + setField('groupId', val)} + singleSelect + closeOnSelect + /> + +
+ + )} + + {isLoading && ( + + )} + + {senderConf?.mainAction && senderConf?.senderFields?.length > 0 && ( +
+ {__('Map Fields', 'bit-integrations')} + {subscriberActions.includes(senderConf?.mainAction) && ( + + )} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('Sender Fields', 'bit-integrations')} +
+
+ + {senderConf?.field_map?.map((itm, i) => ( + + ))} +
+ +
+
+
+ )} + + {triggerAutomationActions.includes(senderConf?.mainAction) && ( +
+ {__('Utilities', 'bit-integrations')} +
+ +
+ )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/Sender/staticData.js b/frontend/src/components/AllIntegrations/Sender/staticData.js new file mode 100644 index 000000000..c71561551 --- /dev/null +++ b/frontend/src/components/AllIntegrations/Sender/staticData.js @@ -0,0 +1,95 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { + name: 'create_or_update_subscriber', + label: __('Create or Update Subscriber', 'bit-integrations'), + is_pro: true + }, + { name: 'update_subscriber', label: __('Update Subscriber', 'bit-integrations'), is_pro: true }, + { name: 'delete_subscriber', label: __('Delete Subscriber', 'bit-integrations'), is_pro: true }, + { + name: 'remove_phone_from_subscriber', + label: __('Remove Phone From Subscriber', 'bit-integrations'), + is_pro: true + }, + { + name: 'add_subscriber_to_group', + label: __('Add Subscriber To Group', 'bit-integrations'), + is_pro: true + }, + { + name: 'remove_subscriber_from_group', + label: __('Remove Subscriber From Group', 'bit-integrations'), + is_pro: true + }, + { name: 'create_group', label: __('Create Group', 'bit-integrations'), is_pro: true }, + { name: 'update_group', label: __('Update Group', 'bit-integrations'), is_pro: true }, + { name: 'delete_group', label: __('Delete Group', 'bit-integrations'), is_pro: true } +] + +// Field-map definitions per action. Only inputs that are NOT a fetchable dropdown live here. +// Group ids come from the group dropdown (conf.groups / conf.groupId), not the field map. +export const SubscriberFields = [ + { key: 'email', label: __('Email', 'bit-integrations'), required: true }, + { key: 'firstname', label: __('First Name', 'bit-integrations'), required: false }, + { key: 'lastname', label: __('Last Name', 'bit-integrations'), required: false }, + { key: 'phone', label: __('Phone', 'bit-integrations'), required: false } +] + +export const UpdateSubscriberFields = [ + { key: 'subscriber_id', label: __('Subscriber ID / Email', 'bit-integrations'), required: true }, + { key: 'firstname', label: __('First Name', 'bit-integrations'), required: false }, + { key: 'lastname', label: __('Last Name', 'bit-integrations'), required: false }, + { key: 'phone', label: __('Phone', 'bit-integrations'), required: false }, + { key: 'subscriber_status', label: __('Subscriber Status', 'bit-integrations'), required: false }, + { key: 'sms_status', label: __('SMS Status', 'bit-integrations'), required: false }, + { + key: 'transactional_email_status', + label: __('Transactional Email Status', 'bit-integrations'), + required: false + } +] + +export const EmailsField = [ + { key: 'emails', label: __('Emails (comma separated)', 'bit-integrations'), required: true } +] + +export const SubscriberIdField = [ + { key: 'subscriber_id', label: __('Subscriber ID / Email', 'bit-integrations'), required: true } +] + +export const GroupTitleField = [ + { key: 'title', label: __('Group Title', 'bit-integrations'), required: true } +] + +// Map an action to its static field-map definition. +export const senderFieldsByAction = { + create_or_update_subscriber: SubscriberFields, + update_subscriber: UpdateSubscriberFields, + delete_subscriber: EmailsField, + remove_phone_from_subscriber: SubscriberIdField, + add_subscriber_to_group: EmailsField, + remove_subscriber_from_group: EmailsField, + create_group: GroupTitleField, + update_group: GroupTitleField, + delete_group: [] +} + +// Actions that need the single-group dropdown. +export const singleGroupActions = [ + 'add_subscriber_to_group', + 'remove_subscriber_from_group', + 'update_group', + 'delete_group' +] + +// Actions that need the multi-group dropdown + custom field fetch. +export const subscriberActions = ['create_or_update_subscriber', 'update_subscriber'] + +// Actions that show the Trigger Automation switch. +export const triggerAutomationActions = [ + 'create_or_update_subscriber', + 'update_subscriber', + 'add_subscriber_to_group' +] diff --git a/frontend/src/components/Flow/New/SelectAction.jsx b/frontend/src/components/Flow/New/SelectAction.jsx index 9259273f9..3c94c1537 100644 --- a/frontend/src/components/Flow/New/SelectAction.jsx +++ b/frontend/src/components/Flow/New/SelectAction.jsx @@ -64,6 +64,7 @@ export default function SelectAction() { { type: 'Freshdesk' }, { type: 'Zoho Desk' }, { type: 'Sendy' }, + { type: 'Sender' }, { type: 'Zoom' }, { type: 'Zoom Webinar' }, { type: 'Fluent Support' }, diff --git a/frontend/src/resource/img/integ/sender.webp b/frontend/src/resource/img/integ/sender.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b2f1f0733c0590a33299170ad83ff659aa68968 GIT binary patch literal 5434 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 literal 0 HcmV?d00001 From e0b78ea52d6453103dbbd95d5980bbe27bed45a1 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 11 Jun 2026 17:30:34 +0600 Subject: [PATCH 2/4] refactor: sender pro actions --- backend/Actions/Sender/RecordApiHelper.php | 83 ++++++++++++------- backend/Actions/Sender/SenderController.php | 22 +++-- .../Sender/SenderAuthorization.jsx | 26 +++--- 3 files changed, 83 insertions(+), 48 deletions(-) diff --git a/backend/Actions/Sender/RecordApiHelper.php b/backend/Actions/Sender/RecordApiHelper.php index 06606790f..09f21317e 100644 --- a/backend/Actions/Sender/RecordApiHelper.php +++ b/backend/Actions/Sender/RecordApiHelper.php @@ -23,49 +23,74 @@ class RecordApiHelper public function __construct($integrationDetails, $integId) { $this->_integrationDetails = $integrationDetails; - $this->_integrationID = $integId; + $this->_integrationID = $integId; } public function execute($fieldValues, $fieldMap) { - $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); $mainAction = $this->_integrationDetails->mainAction ?? ''; $defaultResponse = [ 'success' => false, // translators: %s: Plugin name - 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'), + 'message' => wp_sprintf(__('%s plugin is not installed or activates', 'bit-integrations'), 'Bit Integrations Pro'), ]; - $actionHookMap = [ - 'create_or_update_subscriber' => 'sender_create_or_update_subscriber', - 'update_subscriber' => 'sender_update_subscriber', - 'delete_subscriber' => 'sender_delete_subscriber', - 'remove_phone_from_subscriber' => 'sender_remove_phone_from_subscriber', - 'add_subscriber_to_group' => 'sender_add_subscriber_to_group', - 'remove_subscriber_from_group' => 'sender_remove_subscriber_from_group', - 'create_group' => 'sender_create_group', - 'update_group' => 'sender_update_group', - 'delete_group' => 'sender_delete_group', - ]; + switch ($mainAction) { + case 'create_or_update_subscriber': + $response = Hooks::apply(Config::withPrefix('sender_create_or_update_subscriber'), $defaultResponse, $fieldData, $this->_integrationDetails); - if (!isset($actionHookMap[$mainAction])) { - $response = [ - 'success' => false, - 'message' => __('Invalid action', 'bit-integrations'), - ]; + break; - LogHandler::save($this->_integrationID, ['type' => 'Sender', 'type_name' => 'unknown'], 'error', $response); + case 'update_subscriber': + $response = Hooks::apply(Config::withPrefix('sender_update_subscriber'), $defaultResponse, $fieldData, $this->_integrationDetails); - return $response; - } + break; + + case 'delete_subscriber': + $response = Hooks::apply(Config::withPrefix('sender_delete_subscriber'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + case 'remove_phone_from_subscriber': + $response = Hooks::apply(Config::withPrefix('sender_remove_phone_from_subscriber'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + case 'add_subscriber_to_group': + $response = Hooks::apply(Config::withPrefix('sender_add_subscriber_to_group'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + case 'remove_subscriber_from_group': + $response = Hooks::apply(Config::withPrefix('sender_remove_subscriber_from_group'), $defaultResponse, $fieldData, $this->_integrationDetails); - $response = Hooks::apply( - Config::withPrefix($actionHookMap[$mainAction]), - $defaultResponse, - $fieldData, - $this->_integrationDetails - ); + break; + + case 'create_group': + $response = Hooks::apply(Config::withPrefix('sender_create_group'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + case 'update_group': + $response = Hooks::apply(Config::withPrefix('sender_update_group'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + case 'delete_group': + $response = Hooks::apply(Config::withPrefix('sender_delete_group'), $defaultResponse, $fieldData, $this->_integrationDetails); + + break; + + default: + $response = [ + 'success' => false, + 'message' => __('Invalid action', 'bit-integrations'), + ]; + + break; + } $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; LogHandler::save($this->_integrationID, ['type' => 'Sender', 'type_name' => $mainAction], $responseType, $response); @@ -82,7 +107,7 @@ private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) } $triggerValue = $item->formField; - $actionValue = $item->senderField; + $actionValue = $item->senderField; $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) ? Common::replaceFieldWithValue($item->customValue, $fieldValues) diff --git a/backend/Actions/Sender/SenderController.php b/backend/Actions/Sender/SenderController.php index 2127c4295..232f95289 100644 --- a/backend/Actions/Sender/SenderController.php +++ b/backend/Actions/Sender/SenderController.php @@ -57,7 +57,7 @@ public static function refreshGroups($requestParams) foreach (self::dataRows($response) as $group) { $groups[] = [ 'id' => $group->id ?? '', - 'title' => $group->title ?? ($group->name ?? ''), + 'title' => $group->title ?? '', ]; } @@ -81,16 +81,26 @@ public static function refreshFields($requestParams) wp_send_json_error(__('Failed to fetch fields', 'bit-integrations'), 400); } - $fields = []; + $defaultFields = ['email', 'firstname', 'lastname', 'phone']; + $fields = []; foreach (self::dataRows($response) as $field) { - $key = $field->id ?? ''; - if ($key === '') { + // Sender returns the field token as `name` in {{slug}} form; the subscriber `fields` + // payload must be keyed by the {$slug} personalization token, not the numeric/string id. + $token = isset($field->name) ? str_replace(['{{', '}}'], ['{$', '}'], $field->name) : ''; + if ($token === '') { + continue; + } + + // Default subscriber fields are mapped top-level (email/firstname/lastname/phone), + // so keep them out of the custom-field options to avoid duplicate mapping. + $slug = strtolower(preg_replace('/[{}$\s]/', '', $token)); + if (\in_array($slug, $defaultFields, true)) { continue; } $fields[] = [ - 'key' => (string) $key, - 'label' => $field->name ?? ($field->title ?? (string) $key), + 'key' => $token, + 'label' => $field->title ?? $token, ]; } diff --git a/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx b/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx index 14e7b2dc7..4cd7c3450 100644 --- a/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx +++ b/frontend/src/components/AllIntegrations/Sender/SenderAuthorization.jsx @@ -6,6 +6,19 @@ import Note from '../../Utilities/Note' import TutorialLink from '../../Utilities/TutorialLink' import { authorization } from './SenderCommonFunc' +const tokenUrl = 'https://app.sender.net/settings/tokens' +const note = ` +

${__('Steps to generate an API access token:', 'bit-integrations')}

+ + ` + export default function SenderAuthorization({ senderConf, setSenderConf, @@ -16,7 +29,6 @@ export default function SenderAuthorization({ isInfo }) { const [isAuthorized, setIsAuthorized] = useState(false) - const tokenUrl = 'https://app.sender.net/settings/tokens' const nextPage = () => { setTimeout(() => { @@ -31,18 +43,6 @@ export default function SenderAuthorization({ setSenderConf(newConf) } - const note = ` -

${__('Steps to generate an API access token:', 'bit-integrations')}

- - ` - return (
Date: Thu, 11 Jun 2026 18:04:19 +0600 Subject: [PATCH 3/4] refactor: sender actions files --- backend/Actions/Sender/SenderController.php | 14 +++++++++++- .../AllIntegrations/Sender/Sender.jsx | 9 ++++++++ .../Sender/SenderCommonFunc.js | 6 +++++ .../Sender/SenderIntegLayout.jsx | 22 ++++++++++++------- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/backend/Actions/Sender/SenderController.php b/backend/Actions/Sender/SenderController.php index 232f95289..f1e797c3f 100644 --- a/backend/Actions/Sender/SenderController.php +++ b/backend/Actions/Sender/SenderController.php @@ -27,7 +27,11 @@ public static function senderAuthorize($requestParams) wp_send_json_error(__('API token is required', 'bit-integrations'), 400); } - HttpHelper::get(self::$baseUrl . '/groups', null, self::authHeader($requestParams->api_token)); + $response = HttpHelper::get(self::$baseUrl . '/groups', null, self::authHeader($requestParams->api_token)); + + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } if (HttpHelper::$responseCode >= 200 && HttpHelper::$responseCode < 300) { wp_send_json_success(__('Authorized Successfully', 'bit-integrations'), 200); @@ -49,6 +53,10 @@ public static function refreshGroups($requestParams) $response = HttpHelper::get(self::$baseUrl . '/groups', null, self::authHeader($requestParams->api_token)); + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + if (HttpHelper::$responseCode < 200 || HttpHelper::$responseCode >= 300) { wp_send_json_error(__('Failed to fetch groups', 'bit-integrations'), 400); } @@ -77,6 +85,10 @@ public static function refreshFields($requestParams) $response = HttpHelper::get(self::$baseUrl . '/fields', null, self::authHeader($requestParams->api_token)); + if (is_wp_error($response)) { + wp_send_json_error($response->get_error_message(), 400); + } + if (HttpHelper::$responseCode < 200 || HttpHelper::$responseCode >= 300) { wp_send_json_error(__('Failed to fetch fields', 'bit-integrations'), 400); } diff --git a/frontend/src/components/AllIntegrations/Sender/Sender.jsx b/frontend/src/components/AllIntegrations/Sender/Sender.jsx index 0c1a94a27..dd9625c9f 100644 --- a/frontend/src/components/AllIntegrations/Sender/Sender.jsx +++ b/frontend/src/components/AllIntegrations/Sender/Sender.jsx @@ -10,6 +10,7 @@ import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' import SenderAuthorization from './SenderAuthorization' import { checkMappedFields } from './SenderCommonFunc' import SenderIntegLayout from './SenderIntegLayout' +import { singleGroupActions } from './staticData' function Sender({ formFields, setFlow, flow, allIntegURL }) { const navigate = useNavigate() @@ -33,6 +34,10 @@ function Sender({ formFields, setFlow, flow, allIntegURL }) { }) const saveConfig = () => { + if (singleGroupActions.includes(senderConf?.mainAction) && !senderConf?.groupId) { + toast.error(__('Please select a group', 'bit-integrations')) + return + } if (!checkMappedFields(senderConf)) { toast.error(__('Please map mandatory fields', 'bit-integrations')) return @@ -59,6 +64,10 @@ function Sender({ formFields, setFlow, flow, allIntegURL }) { toast.error(__('Please select an action', 'bit-integrations')) return } + if (singleGroupActions.includes(senderConf?.mainAction) && !senderConf?.groupId) { + toast.error(__('Please select a group', 'bit-integrations')) + return + } if (!checkMappedFields(senderConf)) { toast.error(__('Please map mandatory fields', 'bit-integrations')) return diff --git a/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js b/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js index 38cb9569e..5f603f265 100644 --- a/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js +++ b/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js @@ -3,6 +3,7 @@ import { create } from 'mutative' import toast from 'react-hot-toast' import bitsFetch from '../../../Utils/bitsFetch' import { __ } from '../../../Utils/i18nwrap' +import { singleGroupActions } from './staticData' export const handleInput = (e, senderConf, setSenderConf) => { const { name, value } = e.target @@ -22,6 +23,11 @@ export const generateMappedField = fields => { } export const checkMappedFields = senderConf => { + // Group-target actions require a selected group id, which lives outside the field map. + if (singleGroupActions.includes(senderConf?.mainAction) && !senderConf?.groupId) { + return false + } + const mappedFields = senderConf?.field_map ? senderConf.field_map.filter( mappedField => diff --git a/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx b/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx index 28357838f..860e029b1 100644 --- a/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx @@ -42,6 +42,10 @@ export default function SenderIntegLayout({ draftConf.mainAction = value draftConf.senderFields = fields draftConf.field_map = fields.length ? generateMappedField(fields) : [] + // Clear per-action utilities/group selections so they never leak across an action switch. + draftConf.actions = {} + draftConf.groupId = '' + draftConf.groups = [] }) ) @@ -149,14 +153,16 @@ export default function SenderIntegLayout({ setSenderConf={setSenderConf} /> ))} -
- -
+ {subscriberActions.includes(senderConf?.mainAction) && ( +
+ +
+ )}
)} From ae3544cfa669e3ba7207f690652e8231f30981c3 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Thu, 11 Jun 2026 18:15:12 +0600 Subject: [PATCH 4/4] fix: add error guard --- backend/Actions/Sender/RecordApiHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Actions/Sender/RecordApiHelper.php b/backend/Actions/Sender/RecordApiHelper.php index 09f21317e..af98767f4 100644 --- a/backend/Actions/Sender/RecordApiHelper.php +++ b/backend/Actions/Sender/RecordApiHelper.php @@ -92,7 +92,7 @@ public function execute($fieldValues, $fieldMap) break; } - $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + $responseType = !is_wp_error($response) && isset($response['success']) && $response['success'] ? 'success' : 'error'; LogHandler::save($this->_integrationID, ['type' => 'Sender', 'type_name' => $mainAction], $responseType, $response); return $response;