import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext';
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
function formatUpdatedAt(value) {
if (!value) return 'Not updated yet';
try {
return new Date(value).toLocaleString();
} catch {
return 'Not updated yet';
}
}
function buildProfilePatchPayload(inputs = [], values = {}) {
const provider = {};
const profileInputValues = {};
inputs.forEach((input) => {
const rawValue = String(values[input.key] ?? '').trim();
if (!rawValue) return;
if (BASE_PROFILE_KEYS.has(input.key)) {
provider[input.key] = input.key === 'senderId' ? rawValue.toUpperCase() : rawValue;
return;
}
profileInputValues[input.key] = rawValue;
});
return {
...(Object.keys(provider).length > 0 ? { provider } : {}),
...(Object.keys(profileInputValues).length > 0 ? { profileInputValues } : {}),
};
}
function getInputInitialValues(inputs = []) {
return inputs.reduce((accumulator, input) => {
accumulator[input.key] = input.value || '';
return accumulator;
}, {});
}
function getProfileSummary(profile) {
const parts = [];
const provider = profile?.provider || {};
const missingCount = profile?.executionReadiness?.missingProfileInputs?.length || 0;
if (provider.providerName) parts.push(provider.providerName);
if (provider.senderId) parts.push(`Sender ${provider.senderId}`);
if (provider.dltEntityId) parts.push('DLT ready');
if (missingCount > 0) parts.push(`${missingCount} required field${missingCount === 1 ? '' : 's'} pending`);
return parts.join(' • ') || 'Profile saved. Complete the remaining setup fields to continue.';
}
function isPendingSenderIdProfile(profile) {
const normalizedName = String(profile?.name || '').trim();
const senderId = String(profile?.provider?.senderId || '').trim();
return (profile?.isAutoNamed === true && !senderId) || normalizedName === PENDING_SENDER_ID_PROFILE_NAME;
}
function DeleteProfileModal({ preview, deleting, onCancel, onConfirm }) {
if (!preview) return null;
const impactedTemplates = Array.isArray(preview.impactedTemplates) ? preview.impactedTemplates : [];
return (
Delete cURL Profile
{preview.profile?.name || 'This profile'} will be deleted. Bound templates will be removed, but event definitions will stay.
{impactedTemplates.length > 0 ? (
<>
Affected templates
{impactedTemplates.map((template) => (
{template.eventLabel || template.eventSlug}
{template.status || 'generated'}
{template.templateId && (
DLT Template ID: {template.templateId}
)}
))}
>
) : (
No templates are currently bound to this profile.
)}
);
}
export default function GlobalSms() {
const { businessId } = useParams();
const navigate = useNavigate();
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
const [loading, setLoading] = useState(true);
const [profiles, setProfiles] = useState([]);
const [activeProfileId, setActiveProfileId] = useState(null);
const [saving, setSaving] = useState(false);
const [savingInputs, setSavingInputs] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [formCurl, setFormCurl] = useState('');
const [formSetActive, setFormSetActive] = useState(true);
const [inputForm, setInputForm] = useState({});
const [revealedProfiles, setRevealedProfiles] = useState({});
const [visibleProfileIds, setVisibleProfileIds] = useState({});
const [deletePreview, setDeletePreview] = useState(null);
const [deletingProfileId, setDeletingProfileId] = useState('');
const activeProfile = useMemo(
() => profiles.find((profile) => profile.id === activeProfileId) || null,
[profiles, activeProfileId],
);
const missingInputs = useMemo(
() => activeProfile?.executionReadiness?.missingProfileInputs || [],
[activeProfile?.executionReadiness?.missingProfileInputs],
);
const hasProfiles = profiles.length > 0;
const eventsPath = `/${businessId}/events`;
const loadProfiles = useCallback(async () => {
try {
setLoading(true);
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
const fetchedProfiles = res.data?.profiles || [];
const nextActiveProfileId = res.data?.activeProfileId || null;
const nextActiveProfile = fetchedProfiles.find((profile) => profile.id === nextActiveProfileId) || null;
const nextIsSetupComplete = nextActiveProfile?.executionReadiness?.isSetupComplete === true;
setProfiles(fetchedProfiles);
setActiveProfileId(nextActiveProfileId);
setHasGlobalSms(fetchedProfiles.length > 0);
setIsSetupComplete(nextIsSetupComplete);
return {
activeProfile: nextActiveProfile,
hasProfile: !!nextActiveProfile,
complete: nextIsSetupComplete,
};
} catch {
setError('Failed to load cURL profiles');
setHasGlobalSms(false);
setIsSetupComplete(false);
return { activeProfile: null, hasProfile: false, complete: false };
} finally {
setLoading(false);
}
}, [businessId, setHasGlobalSms, setIsSetupComplete]);
useEffect(() => {
loadProfiles();
}, [loadProfiles]);
useEffect(() => {
setInputForm(getInputInitialValues(missingInputs));
}, [activeProfileId, missingInputs]);
const ensureRevealData = useCallback(async (profileId) => {
if (revealedProfiles[profileId]) return revealedProfiles[profileId];
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/reveal`);
setRevealedProfiles((current) => ({ ...current, [profileId]: res.data }));
return res.data;
}, [businessId, revealedProfiles]);
async function handleSubmit(event) {
event.preventDefault();
if (!formCurl.trim()) return;
setSaving(true);
setError('');
setSuccess('');
const shouldAutoAdvance = !isSetupComplete;
try {
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
rawCurl: formCurl.trim(),
setActive: formSetActive,
});
setFormCurl('');
setFormSetActive(true);
setSuccess('Profile created successfully.');
const nextState = await loadProfiles();
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (err) {
setError(err.response?.data?.error || 'Failed to save cURL profile');
} finally {
setSaving(false);
}
}
async function handleActivate(profileId) {
const shouldAutoAdvance = !isSetupComplete;
setError('');
setSuccess('');
try {
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/activate`);
const nextState = await loadProfiles();
setSuccess('Active profile updated.');
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (err) {
setError(err.response?.data?.error || 'Failed to activate profile');
}
}
async function handleCopyCurl(profile) {
try {
const revealData = await ensureRevealData(profile.id);
const textToCopy = revealData?.rawCurl || profile.maskedCurl || '';
if (!textToCopy) return;
await navigator.clipboard.writeText(textToCopy);
setSuccess(`Copied ${profile.name} cURL.`);
} catch {
setError('Failed to copy the cURL command.');
}
}
async function handleToggleCurl(profile) {
setError('');
if (!visibleProfileIds[profile.id]) {
try {
await ensureRevealData(profile.id);
} catch (err) {
setError(err.response?.data?.error || 'Failed to reveal stored cURL');
return;
}
}
setVisibleProfileIds((current) => ({
...current,
[profile.id]: !current[profile.id],
}));
}
async function handleProviderSubmit(event) {
event.preventDefault();
if (!activeProfileId || missingInputs.length === 0) return;
setSavingInputs(true);
setError('');
setSuccess('');
const shouldAutoAdvance = !isSetupComplete;
try {
const payload = buildProfilePatchPayload(missingInputs, inputForm);
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, payload);
setSuccess('Required profile fields saved.');
const nextState = await loadProfiles();
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (err) {
setError(err.response?.data?.error || 'Failed to save required profile fields');
} finally {
setSavingInputs(false);
}
}
async function handleDeleteRequest(profile) {
setError('');
setSuccess('');
try {
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/delete-impact`);
setDeletePreview(res.data);
} catch (err) {
setError(err.response?.data?.error || 'Failed to load delete impact');
}
}
async function handleDeleteConfirm() {
if (!deletePreview?.profile?.id) return;
setDeletingProfileId(deletePreview.profile.id);
setError('');
setSuccess('');
try {
await apiClient.delete(`/api/businesses/${businessId}/global-sms/profiles/${deletePreview.profile.id}`);
setDeletePreview(null);
setVisibleProfileIds((current) => {
const nextState = { ...current };
delete nextState[deletePreview.profile.id];
return nextState;
});
setRevealedProfiles((current) => {
const nextState = { ...current };
delete nextState[deletePreview.profile.id];
return nextState;
});
await loadProfiles();
setSuccess('Profile deleted successfully.');
} catch (err) {
setError(err.response?.data?.error || 'Failed to delete profile');
} finally {
setDeletingProfileId('');
}
}
if (loading) {
return (
);
}
return (
<>
setDeletePreview(null)}
onConfirm={handleDeleteConfirm}
/>
Omni-channel SMS
Add and activate a validated provider cURL, complete the required profile fields, and then continue to event template setup.
{error && (
{error}
)}
{success && (
{success}
)}
{activeProfile ? (
Active Setup:{' '}
{activeProfile.name}
{activeProfile.executionReadiness?.isSetupComplete ? 'Setup Complete' : 'Missing Information'}
{!activeProfile.executionReadiness?.isSetupComplete ? (
Complete the required fields
) : (
Your active cURL profile is fully configured.
)}
) : hasProfiles ? (
Select an active cURL profile to continue. Your saved profiles are still available below.
) : null}
{hasProfiles && (
Saved Profiles
{profiles.map((profile) => {
const isActive = profile.id === activeProfileId;
const isVisible = visibleProfileIds[profile.id] === true;
const revealedProfile = revealedProfiles[profile.id];
const displayCurl = isVisible ? (revealedProfile?.rawCurl || profile.maskedCurl) : profile.maskedCurl;
return (
{profile.name}
{isActive && (
Active Profile
)}
{profile.executionReadiness?.isSetupComplete ? 'Ready' : 'Needs Fields'}
Updated: {formatUpdatedAt(profile.updatedAt)}
{getProfileSummary(profile)}
Stored cURL
{displayCurl || 'No cURL stored.'}
{!isActive && (
)}
);
})}
)}
Add New Profile
Paste a provider cURL exactly once. After validation, the stored cURL becomes immutable and can only be replaced by creating a new profile.
{!hasProfiles && (
Start by adding a cURL profile
This becomes the base for validating provider details and unlocking event template generation.
)}
>
);
}