@@ -113,8 +128,29 @@ export default function RegisterBusinessModal({ onClose }) {
✓
Business Added!
-
Brand detected:
-
{brandName}
+
Brand detected and ready for onboarding.
+
+
+
+ {successImage ? (
+

+ ) : (
+
+ {successName?.[0]?.toUpperCase() || 'B'}
+
+ )}
+
+
+
{successName}
+ {successDomain && (
+
{successDomain}
+ )}
+ {successTagline && (
+
{successTagline}
+ )}
+
+
+
+ );
+}
+
+function BusinessCreatedModal({ business, onClose }) {
+ const name = getBusinessName(business);
+ const domain = getBusinessDomain(business);
+ const tagline = getBusinessTagline(business);
+ const image = getBusinessImage(business);
+
+ return (
+
+
+
✓
+
Business Added!
+
Your sales channel has been connected successfully.
+
+
+
+ {image ? (
+

+ ) : (
+
{name?.[0]?.toUpperCase() || 'B'}
+ )}
+
+
+
{name}
+ {domain &&
{domain}
}
+ {tagline &&
{tagline}
}
+
+
+
+
+ Done
+
+
+
+ );
+}
+
export default function Businesses() {
const navigate = useNavigate();
const { setActiveBusiness } = useBusiness();
const [businesses, setBusinesses] = useState([]);
+ const [salesChannels, setSalesChannels] = useState([]);
const [loading, setLoading] = useState(true);
+ const [salesChannelsStatus, setSalesChannelsStatus] = useState('loading');
+ const [salesChannelsError, setSalesChannelsError] = useState('');
const [selectingBusinessId, setSelectingBusinessId] = useState('');
+ const [creatingSalesChannelId, setCreatingSalesChannelId] = useState('');
+ const [createdBusiness, setCreatedBusiness] = useState(null);
const [showModal, setShowModal] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState('');
- async function load() {
+ const configuredApplicationIds = useMemo(() => (
+ new Set(
+ businesses
+ .map((business) => String(business?.applicationId || '').trim())
+ .filter(Boolean)
+ )
+ ), [businesses]);
+
+ const availableSalesChannels = useMemo(() => (
+ salesChannels.filter((channel) => !configuredApplicationIds.has(getChannelId(channel)))
+ ), [configuredApplicationIds, salesChannels]);
+
+ const loadBusinesses = useCallback(async () => {
+ const res = await apiClient.get('/api/businesses');
+ setBusinesses(res.data.businesses || []);
+ }, []);
+
+ const loadSalesChannels = useCallback(async () => {
+ setSalesChannelsStatus('loading');
+ const res = await apiClient.get('/api/businesses/sales-channels');
+ const channels = normalizeChannelsPayload(res.data).filter(isChannelActive);
+ setSalesChannels(channels);
+ setSalesChannelsStatus('success');
+ }, []);
+
+ const load = useCallback(async () => {
setLoading(true);
+ setError('');
+ setSalesChannelsError('');
+
try {
- const res = await apiClient.get('/api/businesses');
- setBusinesses(res.data.businesses || []);
- } catch {
- setError('Failed to load businesses');
+ const [businessesRes, salesChannelsRes] = await Promise.allSettled([
+ loadBusinesses(),
+ loadSalesChannels(),
+ ]);
+
+ if (businessesRes.status === 'rejected') {
+ setError('Failed to load businesses');
+ }
+
+ if (salesChannelsRes.status === 'rejected') {
+ setSalesChannels([]);
+ setSalesChannelsStatus('error');
+ setSalesChannelsError(
+ salesChannelsRes.reason?.response?.data?.error || 'Active sales channels could not be loaded right now.'
+ );
+ }
} finally {
setLoading(false);
}
- }
+ }, [loadBusinesses, loadSalesChannels]);
- useEffect(() => { load(); }, []);
+ useEffect(() => { load(); }, [load]);
async function handleSelect(biz) {
setSelectingBusinessId(biz.businessId);
@@ -74,6 +218,26 @@ export default function Businesses() {
}
}
+ async function handleCreateFromSalesChannel(channel) {
+ const salesChannelId = getChannelId(channel);
+ if (!salesChannelId) return;
+
+ setCreatingSalesChannelId(salesChannelId);
+ setError('');
+ setSalesChannelsError('');
+
+ try {
+ const res = await apiClient.post('/api/businesses', { salesChannelId });
+ setCreatedBusiness(res.data);
+ await Promise.all([loadBusinesses(), loadSalesChannels()]);
+ setSalesChannelsStatus('success');
+ } catch (err) {
+ setError(err.response?.data?.error || 'Failed to add business from sales channel');
+ } finally {
+ setCreatingSalesChannelId('');
+ }
+ }
+
async function handleDelete() {
if (!deleteTarget) return;
setDeleting(true);
@@ -83,6 +247,7 @@ export default function Businesses() {
await load();
} catch (err) {
setError(err.response?.data?.error || 'Failed to delete business');
+ } finally {
setDeleting(false);
}
}
@@ -95,40 +260,17 @@ export default function Businesses() {
);
}
- // ── NO BUSINESSES YET ──────────────────────────────────────────────────────
- if (businesses.length === 0 && !showModal) {
- return (
-
-
-
- S
-
-
SMS Template Extension
-
- Generate TRAI-compliant SMS templates for your Fynd store. Add your first business to get started.
-
-
setShowModal(true)}
- className="px-8 py-3 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white font-medium shadow-sm transition-all focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600"
- >
- Add Your First Business
-
-
- {showModal &&
{ setShowModal(false); load(); }} />}
-
- );
- }
-
- // ── BUSINESS LIST ──────────────────────────────────────────────────────────
return (
-
- {/* Header */}
-
Your Businesses
-
Select a business to manage its SMS templates.
+
+ {businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business'}
+
+
+ Add directly from an active sales channel, or use the existing fallback modal for manual testing.
+
setShowModal(true)}
@@ -145,54 +287,139 @@ export default function Businesses() {
)}
-
- {businesses.map(biz => (
-
+
+
+
Active Sales Channels
+
Choose a live channel to create its business record directly.
+
+
setShowModal(true)}
+ className="text-sm font-semibold text-indigo-600 hover:text-indigo-700 transition"
>
- handleSelect(biz)}
- disabled={selectingBusinessId === biz.businessId}
- >
-
-
- {biz.brandName?.[0]?.toUpperCase() || 'B'}
-
-
-
{biz.brandName}
-
{biz.domain}
+ Use fallback modal instead
+
+
+
+ {salesChannelsError && (
+
+ {salesChannelsError}
+
+ )}
+
+ {salesChannelsStatus === 'loading' ? (
+
+
+
Loading active sales channels…
+
+ ) : availableSalesChannels.length > 0 ? (
+
+ {availableSalesChannels.map((channel) => {
+ const channelId = getChannelId(channel);
+
+ return (
+ handleCreateFromSalesChannel(channel)}
+ disabled={creatingSalesChannelId === channelId}
+ loadingLabel="Creating business from this sales channel…"
+ footer={getBusinessDomain(channel) ? `Sales channel • ${getBusinessDomain(channel)}` : 'Sales channel ready'}
+ />
+ );
+ })}
+
+ ) : (
+
+
No active sales channels are available to add right now.
+
Already-configured channels are hidden here to avoid duplicate onboarding. You can still use the fallback Add Business modal for manual testing.
+
+ )}
+
+
+
+
+
Configured Businesses
+
Select a business to manage its SMS templates.
+
+
+ {businesses.length > 0 ? (
+
+ {businesses.map((biz) => (
+
+
handleSelect(biz)}
+ disabled={selectingBusinessId === biz.businessId}
+ >
+
+
+ {getBusinessImage(biz) ? (
+
})
+ ) : (
+
+ {getBusinessName(biz)?.[0]?.toUpperCase() || 'B'}
+
+ )}
+
+
+
{getBusinessName(biz)}
+ {getBusinessDomain(biz) && (
+
{getBusinessDomain(biz)}
+ )}
+ {getBusinessTagline(biz) && (
+
{getBusinessTagline(biz)}
+ )}
+
+ Added {new Date(biz.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
+
+ {selectingBusinessId === biz.businessId && (
+
+
+ Opening…
+
+ )}
+
+
+
+
+ Click to manage →
+ { e.stopPropagation(); setDeleteTarget(biz); }}
+ className="text-xs text-red-500 hover:text-red-700 font-medium transition"
+ >
+ Delete
+
-
- Added {new Date(biz.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
-
- {selectingBusinessId === biz.businessId && (
-
-
- Opening…
-
- )}
-
-
- Click to manage →
- { e.stopPropagation(); setDeleteTarget(biz); }}
- className="text-xs text-red-500 hover:text-red-700 font-medium transition"
- >
- Delete
-
-
+ ))}
- ))}
-
+ ) : (
+
+
No configured businesses yet.
+
Start from an active sales channel above, or use the Add Business modal as a fallback.
+
+ )}
+
- {showModal &&
{ setShowModal(false); load(); }} />}
+ {showModal && (
+ { setShowModal(false); load(); }}
+ initialSalesChannels={availableSalesChannels}
+ initialSalesChannelsStatus={salesChannelsStatus}
+ initialSalesChannelsError={salesChannelsError}
+ />
+ )}
+ {createdBusiness && setCreatedBusiness(null)} />}
{deleteTarget && (
setDeleteTarget(null)}
onConfirm={handleDelete}
deleting={deleting}
diff --git a/client/src/utils/businessProfile.js b/client/src/utils/businessProfile.js
new file mode 100644
index 0000000..6e06e94
--- /dev/null
+++ b/client/src/utils/businessProfile.js
@@ -0,0 +1,64 @@
+function normalizeList(value) {
+ return Array.isArray(value) ? value.filter(Boolean) : [];
+}
+
+export function normalizeChannelsPayload(data) {
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.salesChannels)) return data.salesChannels;
+ if (Array.isArray(data?.channels)) return data.channels;
+ return [];
+}
+
+export function getChannelId(channel) {
+ return channel?.salesChannelId || channel?.id || channel?._id || '';
+}
+
+export function isChannelActive(channel) {
+ const status = String(channel?.status || channel?.state || '').toLowerCase();
+
+ if (typeof channel?.isActive === 'boolean') return channel.isActive;
+ if (typeof channel?.active === 'boolean') return channel.active;
+ if (typeof channel?.enabled === 'boolean') return channel.enabled;
+ if (status) return ['active', 'enabled', 'live', 'connected'].includes(status);
+
+ return true;
+}
+
+export function getBusinessName(entity) {
+ return entity?.brandName || entity?.name || entity?.title || 'Business';
+}
+
+export function getBusinessDomain(entity) {
+ const directDomain = String(entity?.domain || '').trim();
+ if (directDomain) return directDomain;
+
+ const websiteUrl = String(entity?.websiteUrl || entity?.url || '').trim();
+ if (!websiteUrl) return '';
+
+ return websiteUrl
+ .replace(/^https?:\/\//i, '')
+ .replace(/^www\./i, '')
+ .replace(/\/.*$/, '');
+}
+
+export function getBusinessTagline(entity) {
+ const taglines = normalizeList(entity?.taglines);
+ const firstTagline = taglines.find((tagline) => String(tagline || '').trim());
+ if (firstTagline) return String(firstTagline).trim();
+
+ const fallback = entity?.tagline || entity?.description || entity?.metaDescription || '';
+ return String(fallback).trim();
+}
+
+export function getBusinessImage(entity) {
+ const relevantImage = normalizeList(entity?.relevantImagePaths)[0];
+ if (relevantImage) return relevantImage;
+
+ return (
+ entity?.imageUrl
+ || entity?.logoUrl
+ || entity?.brandImageUrl
+ || entity?.image
+ || ''
+ );
+}
diff --git a/server/routes/businesses.js b/server/routes/businesses.js
index fd052ff..a4e3471 100644
--- a/server/routes/businesses.js
+++ b/server/routes/businesses.js
@@ -279,6 +279,30 @@ function normalizeSalesChannel(application = {}, domains = []) {
};
}
+function getBusinessPreviewSummary(source = {}) {
+ const taglines = Array.isArray(source?.taglines)
+ ? source.taglines.map((tagline) => normalizeText(tagline)).filter(Boolean)
+ : [];
+ const relevantImagePaths = Array.isArray(source?.relevantImagePaths)
+ ? source.relevantImagePaths.map((path) => normalizeText(path)).filter(Boolean)
+ : [];
+
+ return {
+ previewTagline: taglines[0] || '',
+ previewImagePath: relevantImagePaths[0] || '',
+ };
+}
+
+function mergeBusinessSummary(baseBusiness = {}, context = null) {
+ const previewSummary = getBusinessPreviewSummary(context || baseBusiness);
+
+ return {
+ ...baseBusiness,
+ previewTagline: normalizeText(baseBusiness.previewTagline) || previewSummary.previewTagline,
+ previewImagePath: normalizeText(baseBusiness.previewImagePath) || previewSummary.previewImagePath,
+ };
+}
+
async function getPlatformClient(req, companyId) {
if (req?.platformClient) return req.platformClient;
return getPlatformClientForCompany(companyId);
@@ -881,8 +905,20 @@ function getMissingMandatoryProviderFields(provider = {}) {
// GET /api/businesses
router.get('/', async (req, res) => {
try {
- const businesses = await getIndex(getCompanyId(req));
- res.json({ businesses });
+ const merchantId = getCompanyId(req);
+ const businesses = await getIndex(merchantId);
+ const hydratedBusinesses = await Promise.all(
+ businesses.map(async (business) => {
+ if (normalizeText(business.previewTagline) || normalizeText(business.previewImagePath)) {
+ return mergeBusinessSummary(business);
+ }
+
+ const context = await fetchJSON(businessRoot(merchantId, business.businessId), 'context').catch(() => null);
+ return mergeBusinessSummary(business, context);
+ })
+ );
+
+ res.json({ businesses: hydratedBusinesses });
} catch (err) {
res.status(500).json({ error: err.message });
}
@@ -900,7 +936,7 @@ router.get('/sales-channels', async (req, res) => {
const applications = await listAllSalesChannels(platformClient);
const salesChannels = applications
.map((application) => normalizeSalesChannel(application))
- .filter((channel) => channel.id && channel.name)
+ .filter((channel) => channel.id && channel.name && channel.isActive)
.sort((left, right) => left.name.localeCompare(right.name));
res.json({ salesChannels });
@@ -1020,12 +1056,15 @@ router.post('/', async (req, res) => {
await uploadJSON(bizRoot, 'events', { events: DEFAULT_EVENTS });
// 6. Update index.json
+ const previewSummary = getBusinessPreviewSummary(contextJson);
businesses.push({
businessId,
companyId: merchantId,
applicationId,
brandName: contextJson.brandName,
domain: contextJson.domain,
+ previewTagline: previewSummary.previewTagline,
+ previewImagePath: previewSummary.previewImagePath,
createdAt: contextJson.createdAt,
updatedAt: contextJson.updatedAt,
});