diff --git a/client/src/context/BusinessContext.jsx b/client/src/context/BusinessContext.jsx
index 9be6a4c..3b6bb03 100644
--- a/client/src/context/BusinessContext.jsx
+++ b/client/src/context/BusinessContext.jsx
@@ -11,17 +11,49 @@ export function BusinessProvider({ children }) {
const [activeBusiness, setActiveBusinessState] = useState(null);
const [hasGlobalSms, setHasGlobalSms] = useState(false);
const [isSetupComplete, setIsSetupComplete] = useState(false);
+ const [hasSelectedTemplates, setHasSelectedTemplates] = useState(false);
const [loading, setLoading] = useState(true);
- const updateReadyState = useCallback((activeProfile) => {
+ const updateReadyState = useCallback((activeProfile, templates = []) => {
const hasProfile = !!activeProfile;
setHasGlobalSms(hasProfile);
const p = activeProfile?.provider || {};
const nextIsSetupComplete = hasProfile && !!p.providerName && !!p.senderId && !!p.dltEntityId;
setIsSetupComplete(nextIsSetupComplete);
- return nextIsSetupComplete;
+ const nextHasSelectedTemplates = Array.isArray(templates)
+ ? templates.some((template) => !!template?.selectedTemplate)
+ : false;
+ setHasSelectedTemplates(nextHasSelectedTemplates);
+
+ return {
+ hasGlobalSms: hasProfile,
+ isSetupComplete: nextIsSetupComplete,
+ hasSelectedTemplates: nextHasSelectedTemplates,
+ };
}, []);
+ const refreshOnboardingState = useCallback(async (businessIdOverride) => {
+ const targetBusinessId = businessIdOverride || activeBusiness?.businessId;
+
+ if (!targetBusinessId) {
+ setHasGlobalSms(false);
+ setIsSetupComplete(false);
+ setHasSelectedTemplates(false);
+ return {
+ hasGlobalSms: false,
+ isSetupComplete: false,
+ hasSelectedTemplates: false,
+ };
+ }
+
+ const [smsRes, templatesRes] = await Promise.all([
+ apiClient.get(`/api/businesses/${targetBusinessId}/global-sms/active`).catch(() => ({ data: {} })),
+ apiClient.get(`/api/businesses/${targetBusinessId}/templates`).catch(() => ({ data: { templates: [] } })),
+ ]);
+
+ return updateReadyState(smsRes.data?.activeProfile, templatesRes.data?.templates || []);
+ }, [activeBusiness?.businessId, updateReadyState]);
+
// On mount: rehydrate from sessionStorage and refresh from API
useEffect(() => {
async function rehydrate() {
@@ -37,10 +69,13 @@ export function BusinessProvider({ children }) {
const [bizRes, smsRes] = await Promise.all([
apiClient.get(`/api/businesses/${businessId}`),
- apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} }))
+ Promise.all([
+ apiClient.get(`/api/businesses/${businessId}/global-sms/active`).catch(() => ({ data: {} })),
+ apiClient.get(`/api/businesses/${businessId}/templates`).catch(() => ({ data: { templates: [] } })),
+ ]),
]);
setActiveBusinessState(bizRes.data);
- updateReadyState(smsRes.data?.activeProfile);
+ updateReadyState(smsRes[0].data?.activeProfile, smsRes[1].data?.templates || []);
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
businessId,
companyId: runtimeCompanyId || companyId || '',
@@ -51,6 +86,7 @@ export function BusinessProvider({ children }) {
setActiveBusinessState(null);
setHasGlobalSms(false);
setIsSetupComplete(false);
+ setHasSelectedTemplates(false);
} finally {
setLoading(false);
}
@@ -65,19 +101,21 @@ export function BusinessProvider({ children }) {
companyId: getRuntimeCompanyId(),
}));
try {
- const smsRes = await apiClient.get(`/api/businesses/${business.businessId}/global-sms/active`);
- return updateReadyState(smsRes.data?.activeProfile);
+ const progress = await refreshOnboardingState(business.businessId);
+ return progress.isSetupComplete;
} catch {
setHasGlobalSms(false);
setIsSetupComplete(false);
+ setHasSelectedTemplates(false);
return false;
}
- }, [updateReadyState]);
+ }, [refreshOnboardingState]);
const clearBusiness = useCallback(() => {
setActiveBusinessState(null);
setHasGlobalSms(false);
setIsSetupComplete(false);
+ setHasSelectedTemplates(false);
sessionStorage.removeItem(SESSION_KEY);
}, []);
@@ -85,7 +123,18 @@ export function BusinessProvider({ children }) {
return (
{children}
diff --git a/client/src/pages/Events.jsx b/client/src/pages/Events.jsx
index d02429b..120e795 100644
--- a/client/src/pages/Events.jsx
+++ b/client/src/pages/Events.jsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
-import { useParams } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import apiClient from '../api/client';
+import { useBusiness } from '../context/BusinessContext';
const MAX_SMS_LENGTH = 160;
const DLT_VARIABLE_OPTIONS = [
@@ -99,6 +100,8 @@ function buildTemplateUiState(templates = []) {
export default function Events() {
const { businessId } = useParams();
+ const navigate = useNavigate();
+ const { hasSelectedTemplates, refreshOnboardingState } = useBusiness();
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [newLabel, setNewLabel] = useState('');
@@ -259,16 +262,21 @@ export default function Events() {
async function handleSelect(slug, variant, variantIndex) {
const variantKey = getVariantKey(slug, variantIndex);
+ const shouldAutoAdvance = !hasSelectedTemplates;
setSelectingVariantKey(variantKey);
setError('');
try {
await apiClient.post(`/api/businesses/${businessId}/templates/${slug}/select`, { selectedVariant: variant });
+ await refreshOnboardingState(businessId).catch(() => null);
setVariants((currentVariants) => ({ ...currentVariants, [slug]: [] }));
setVariantDrafts((currentDrafts) => removeDraftsForSlug(currentDrafts, slug));
setOpenVariableMenuKey('');
setActiveCaretVariantKey('');
setGenState((state) => ({ ...state, [slug]: 'selected' }));
+ if (shouldAutoAdvance) {
+ navigate(`/${businessId}/templates`);
+ }
} catch (err) {
setError(err.response?.data?.error || 'Failed to select template');
} finally {
diff --git a/client/src/pages/GlobalSms.jsx b/client/src/pages/GlobalSms.jsx
index 9467f42..22d5391 100644
--- a/client/src/pages/GlobalSms.jsx
+++ b/client/src/pages/GlobalSms.jsx
@@ -24,6 +24,7 @@ export default function GlobalSms() {
// Form state for Missing Provider Fields
const [providerForm, setProviderForm] = useState({ providerName: '', senderId: '', dltEntityId: '' });
const [savingProvider, setSavingProvider] = useState(false);
+ const eventsPath = `/${businessId}/events`;
const loadProfiles = useCallback(async () => {
try {
@@ -47,8 +48,13 @@ export default function GlobalSms() {
senderId: p.senderId || '',
dltEntityId: p.dltEntityId || '',
});
+
+ return { activeProfile, hasProfile, complete };
} catch {
setError('Failed to load cURL profiles');
+ setHasGlobalSms(false);
+ setIsSetupComplete(false);
+ return { activeProfile: null, hasProfile: false, complete: false };
} finally {
setLoading(false);
}
@@ -89,6 +95,7 @@ export default function GlobalSms() {
setSaving(true);
setError('');
setSuccess('');
+ const shouldAutoAdvance = !isSetupComplete;
try {
if (editingId) {
@@ -105,10 +112,13 @@ export default function GlobalSms() {
});
setSuccess('Profile created successfully.');
}
- await loadProfiles();
+ const nextState = await loadProfiles();
setFormName('');
setFormCurl('');
setEditingId(null);
+ if (shouldAutoAdvance && nextState.complete) {
+ navigate(eventsPath);
+ }
} catch (err) {
setError(err.response?.data?.error || 'Failed to save cURL profile');
} finally {
@@ -127,9 +137,13 @@ export default function GlobalSms() {
}
async function handleActivate(id) {
+ const shouldAutoAdvance = !isSetupComplete;
try {
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${id}/activate`);
- await loadProfiles();
+ const nextState = await loadProfiles();
+ if (shouldAutoAdvance && nextState.complete) {
+ navigate(eventsPath);
+ }
} catch (err) {
setError(err.response?.data?.error || 'Failed to activate profile');
}
@@ -141,6 +155,7 @@ export default function GlobalSms() {
setSavingProvider(true);
setError('');
setSuccess('');
+ const shouldAutoAdvance = !isSetupComplete;
try {
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, {
@@ -151,7 +166,10 @@ export default function GlobalSms() {
}
});
setSuccess('Provider details saved successfully!');
- await loadProfiles();
+ const nextState = await loadProfiles();
+ if (shouldAutoAdvance && nextState.complete) {
+ navigate(eventsPath);
+ }
} catch (err) {
setError(err.response?.data?.error || 'Failed to save provider details');
} finally {
@@ -169,37 +187,11 @@ export default function GlobalSms() {
return (
- {/* Header & Stepper */}
-
Setup configuration
-
+
Omni-channel SMS
+
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 && (
@@ -300,7 +292,7 @@ export default function GlobalSms() {
Your active cURL profile is fully configured.