+ 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 00000000..dd9625c9
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Sender/Sender.jsx
@@ -0,0 +1,133 @@
+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'
+import { singleGroupActions } from './staticData'
+
+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 (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
+ }
+
+ 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 (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
+ }
+ 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 00000000..70cb9773
--- /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 00000000..4cd7c345
--- /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'
+
+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,
+ step,
+ setstep,
+ loading,
+ setLoading,
+ isInfo
+}) {
+ const [isAuthorized, setIsAuthorized] = useState(false)
+
+ 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)
+ }
+
+ 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 00000000..5f603f26
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Sender/SenderCommonFunc.js
@@ -0,0 +1,111 @@
+/* eslint-disable no-else-return */
+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
+
+ 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 => {
+ // 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 =>
+ !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 00000000..fd7f11cd
--- /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 00000000..860e029b
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/Sender/SenderIntegLayout.jsx
@@ -0,0 +1,184 @@
+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) : []
+ // Clear per-action utilities/group selections so they never leak across an action switch.
+ draftConf.actions = {}
+ draftConf.groupId = ''
+ draftConf.groups = []
+ })
+ )
+
+ 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) => (
+
+ ))}
+ {subscriberActions.includes(senderConf?.mainAction) && (
+
+
+
+ )}
+
+
+ )}
+
+ {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 00000000..c7156155
--- /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 2f70cded..a9816d09 100644
--- a/frontend/src/components/Flow/New/SelectAction.jsx
+++ b/frontend/src/components/Flow/New/SelectAction.jsx
@@ -206,6 +206,7 @@ export default function SelectAction() {
{ type: 'Asgaros Forum', logo: 'asgaros', is_pro: true },
{ type: 'B2BKing', is_pro: true },
{ type: 'User Registration & Membership', logo: 'userRegistrationMembership', is_pro: true },
+ { type: 'Sender', is_pro: true },
{ type: 'MainWP', is_pro: true },
]
diff --git a/frontend/src/resource/img/integ/sender.webp b/frontend/src/resource/img/integ/sender.webp
new file mode 100644
index 00000000..1b2f1f07
Binary files /dev/null and b/frontend/src/resource/img/integ/sender.webp differ