diff --git a/client/src/App.jsx b/client/src/App.jsx
index 0767099..7847e48 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -36,7 +36,7 @@ function SubLayout({ children }) {
// Guard: redirect to / if no active business in session.
// Also enforce cURL-first: only the cURL profile route is available until an active profile exists.
function BusinessGuard({ children, isGlobalSmsRoute }) {
- const { activeBusinessId, loading, hasGlobalSms } = useBusiness();
+ const { activeBusinessId, loading, isSetupComplete } = useBusiness();
const location = useLocation();
if (loading) {
@@ -51,7 +51,7 @@ function BusinessGuard({ children, isGlobalSmsRoute }) {
return ;
}
- if (!hasGlobalSms && !isGlobalSmsRoute) {
+ if (!isSetupComplete && !isGlobalSmsRoute) {
return ;
}
diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx
index 5bac79d..3bfcd00 100644
--- a/client/src/components/Sidebar.jsx
+++ b/client/src/components/Sidebar.jsx
@@ -29,9 +29,9 @@ export default function Sidebar() {
const navigate = useNavigate();
const navItems = [
- { id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Global SMS cURL' },
- { id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' },
- { id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' },
+ { id: 'globalSms', to: `/${activeBusinessId}/global-sms`, label: 'Omni-channel SMS' },
+ { id: 'events', to: `/${activeBusinessId}/events`, label: 'Events' },
+ { id: 'templates', to: `/${activeBusinessId}/templates`, label: 'Templates' },
];
function handleSwitch() {
@@ -70,10 +70,9 @@ export default function Sidebar() {
key={id}
to={to}
className={({ isActive }) =>
- `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${
- isActive
- ? 'bg-refresh-hover text-primary-blue'
- : 'text-text-muted hover:text-text-primary hover:bg-row-hover'
+ `flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ${isActive
+ ? 'bg-refresh-hover text-primary-blue'
+ : 'text-text-muted hover:text-text-primary hover:bg-row-hover'
}`
}
>
diff --git a/client/src/context/BusinessContext.jsx b/client/src/context/BusinessContext.jsx
index 6ffe7f9..64bc56a 100644
--- a/client/src/context/BusinessContext.jsx
+++ b/client/src/context/BusinessContext.jsx
@@ -10,8 +10,16 @@ const SESSION_KEY = 'sms_active_business';
export function BusinessProvider({ children }) {
const [activeBusiness, setActiveBusinessState] = useState(null);
const [hasGlobalSms, setHasGlobalSms] = useState(false);
+ const [isSetupComplete, setIsSetupComplete] = useState(false);
const [loading, setLoading] = useState(true);
+ const updateReadyState = useCallback((activeProfile) => {
+ const hasProfile = !!activeProfile;
+ setHasGlobalSms(hasProfile);
+ const p = activeProfile?.provider || {};
+ setIsSetupComplete(hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId);
+ }, []);
+
// On mount: rehydrate from sessionStorage and refresh from API
useEffect(() => {
async function rehydrate() {
@@ -30,7 +38,7 @@ export function BusinessProvider({ children }) {
apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} }))
]);
setActiveBusinessState(bizRes.data);
- setHasGlobalSms(!!smsRes.data?.activeProfile);
+ updateReadyState(smsRes.data?.activeProfile);
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
businessId,
companyId: runtimeCompanyId || companyId || '',
@@ -40,6 +48,7 @@ export function BusinessProvider({ children }) {
sessionStorage.removeItem(SESSION_KEY);
setActiveBusinessState(null);
setHasGlobalSms(false);
+ setIsSetupComplete(false);
} finally {
setLoading(false);
}
@@ -55,15 +64,17 @@ export function BusinessProvider({ children }) {
}));
try {
const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`);
- setHasGlobalSms(!!smsRes.data?.activeProfile);
+ updateReadyState(smsRes.data?.activeProfile);
} catch {
setHasGlobalSms(false);
+ setIsSetupComplete(false);
}
}, []);
const clearBusiness = useCallback(() => {
setActiveBusinessState(null);
setHasGlobalSms(false);
+ setIsSetupComplete(false);
sessionStorage.removeItem(SESSION_KEY);
}, []);
@@ -71,7 +82,7 @@ export function BusinessProvider({ children }) {
return (
{children}
diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx
index 61fdde0..9467f42 100644
--- a/client/src/pages/GlobalSms.jsx
+++ b/client/src/pages/GlobalSms.jsx
@@ -1,11 +1,12 @@
import { useState, useEffect, useCallback } from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams, useNavigate } from 'react-router-dom';
import apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext';
export default function GlobalSms() {
const { businessId } = useParams();
- const { setHasGlobalSms } = useBusiness();
+ const navigate = useNavigate();
+ const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
const [loading, setLoading] = useState(true);
const [profiles, setProfiles] = useState([]);
const [activeProfileId, setActiveProfileId] = useState(null);
@@ -14,30 +15,56 @@ export default function GlobalSms() {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
- // Form state for Create / Edit
+ // Form state for Create / Edit Profile
const [editingId, setEditingId] = useState(null);
const [formName, setFormName] = useState('');
const [formCurl, setFormCurl] = useState('');
const [formSetActive, setFormSetActive] = useState(true);
+ // Form state for Missing Provider Fields
+ const [providerForm, setProviderForm] = useState({ providerName: '', senderId: '', dltEntityId: '' });
+ const [savingProvider, setSavingProvider] = useState(false);
+
const loadProfiles = useCallback(async () => {
try {
setLoading(true);
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
- setProfiles(res.data.profiles || []);
- setActiveProfileId(res.data.activeProfileId);
- setHasGlobalSms(!!res.data.activeProfileId);
+ const fetchedProfiles = res.data.profiles || [];
+ const fetchActiveId = res.data.activeProfileId;
+ setProfiles(fetchedProfiles);
+ setActiveProfileId(fetchActiveId);
+
+ const activeProfile = fetchedProfiles.find(p => p.id === fetchActiveId) || null;
+ const hasProfile = !!activeProfile;
+ setHasGlobalSms(hasProfile);
+
+ const p = activeProfile?.provider || {};
+ const complete = hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId;
+ setIsSetupComplete(complete);
+
+ setProviderForm({
+ providerName: p.providerName || '',
+ senderId: p.senderId || '',
+ dltEntityId: p.dltEntityId || '',
+ });
} catch {
setError('Failed to load cURL profiles');
} finally {
setLoading(false);
}
- }, [businessId, setHasGlobalSms]);
+ }, [businessId, setHasGlobalSms, setIsSetupComplete]);
useEffect(() => {
loadProfiles();
}, [loadProfiles]);
+ const activeProfile = profiles.find(p => p.id === activeProfileId) || null;
+ const pData = activeProfile?.provider || {};
+ const missingFields = [];
+ if (activeProfile && !pData.providerName) missingFields.push('providerName');
+ if (activeProfile && !pData.senderId) missingFields.push('senderId');
+ if (activeProfile && !pData.dltEntityId) missingFields.push('dltEntityId');
+
function handleAddClick() {
setEditingId(null);
setFormName('');
@@ -51,7 +78,7 @@ export default function GlobalSms() {
setEditingId(profile.id);
setFormName(profile.name);
setFormCurl(profile.rawCurl);
- setFormSetActive(false); // only matters for create
+ setFormSetActive(false);
setError('');
setSuccess('');
}
@@ -108,6 +135,30 @@ export default function GlobalSms() {
}
}
+ async function handleProviderSubmit(e) {
+ e.preventDefault();
+ if (!activeProfileId) return;
+ setSavingProvider(true);
+ setError('');
+ setSuccess('');
+
+ try {
+ await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, {
+ provider: {
+ providerName: providerForm.providerName,
+ senderId: providerForm.senderId.toUpperCase(),
+ dltEntityId: providerForm.dltEntityId,
+ }
+ });
+ setSuccess('Provider details saved successfully!');
+ await loadProfiles();
+ } catch (err) {
+ setError(err.response?.data?.error || 'Failed to save provider details');
+ } finally {
+ setSavingProvider(false);
+ }
+ }
+
if (loading) {
return (
@@ -118,12 +169,37 @@ export default function GlobalSms() {
return (
- {/* Header */}
+ {/* Header & Stepper */}
-
cURL Profiles
-
- Manage the cURL commands used to generate and test SMS templates. The active profile will be used across the application.
+
Setup configuration
+
+ Complete this flow to configure your cURL profile and brand provider data. You must finish setup before generating templates.
+
+ {/* CSS Stepper */}
+
+ {[
+ { label: 'Add / Select Profile', done: !!activeProfile, active: !activeProfile },
+ { label: 'Validate cURL', done: !!activeProfile, active: false },
+ { label: 'Complete Fields', done: isSetupComplete, active: !!activeProfile && !isSetupComplete },
+ { label: 'Ready', done: isSetupComplete, active: isSetupComplete }
+ ].map((step, idx) => (
+
+
+ {step.done ? '✓' : idx + 1}
+
+
+ {step.label}
+
+
+ ))}
+
{error && (
@@ -139,8 +215,105 @@ export default function GlobalSms() {
)}
+ {/* Active Profile Setup Review Block */}
+ {activeProfile && (
+
+
+
Active Setup: {activeProfile.name}
+ {isSetupComplete ? (
+ Setup Complete
+ ) : (
+ Missing Information
+ )}
+
+
+
+
+
Parsed Provider Data:
+
+ -
+ Provider:
+ {pData.providerName || Missing}
+
+ -
+ Sender ID:
+ {pData.senderId || Missing}
+
+ -
+ Entity ID:
+ {pData.dltEntityId || Missing}
+
+ -
+ Auth Key:
+ {pData.authKey ? '••••••••' : 'None setup'}
+
+
+
+
+ {!isSetupComplete && (
+
+
Please fill in the missing fields:
+
+
+ )}
+
+ {isSetupComplete && (
+
+
Your active cURL profile is fully configured.
+
+
+ )}
+
+
+ )}
+
{/* Profiles List */}
-
+
+
All Profiles
{profiles.length > 0 ? (
profiles.map(p => {
const isActive = p.id === activeProfileId;