Sidebar unified and fynd sdk backend so we fetch sales channel directly

This commit is contained in:
Ritul Jadhav 2026-03-31 09:59:03 +05:30
parent 926c332c7e
commit 72a8adb142
7 changed files with 1010 additions and 88 deletions

View File

@ -1,19 +1,102 @@
import { useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import apiClient from '../api/client'; import apiClient from '../api/client';
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 [];
}
function getChannelId(channel) {
return channel?.salesChannelId || channel?.id || channel?._id || '';
}
export default function RegisterBusinessModal({ onClose }) { export default function RegisterBusinessModal({ onClose }) {
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [status, setStatus] = useState('idle'); // idle | loading | success | error const [status, setStatus] = useState('idle'); // idle | loading | success | error
const [brandName, setBrandName] = useState(''); const [brandName, setBrandName] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [entryMode, setEntryMode] = useState('sales-channel');
const [salesChannels, setSalesChannels] = useState([]);
const [salesChannelsStatus, setSalesChannelsStatus] = useState('loading'); // loading | success | error
const [salesChannelsError, setSalesChannelsError] = useState('');
const [selectedSalesChannelId, setSelectedSalesChannelId] = useState('');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
let cancelled = false;
async function fetchSalesChannels() {
setSalesChannelsStatus('loading');
setSalesChannelsError('');
try {
const res = await apiClient.get('/api/businesses/sales-channels');
if (cancelled) return;
const channels = normalizeChannelsPayload(res.data);
setSalesChannels(channels);
if (channels.length > 0) {
setSelectedSalesChannelId(getChannelId(channels[0]));
setSalesChannelsStatus('success');
setEntryMode('sales-channel');
} else {
setSalesChannelsStatus('error');
setSalesChannelsError('No sales channels were found for this company yet. You can still enter the website URL manually.');
setEntryMode('manual');
}
} catch (err) {
if (cancelled) return;
setSalesChannels([]);
setSalesChannelsStatus('error');
setSalesChannelsError(err.response?.data?.error || 'Sales channels could not be fetched right now. You can still enter the website URL manually.');
setEntryMode('manual');
}
}
fetchSalesChannels();
return () => {
cancelled = true;
};
}, []);
const filteredSalesChannels = useMemo(() => {
if (!searchQuery.trim()) return salesChannels;
const query = searchQuery.trim().toLowerCase();
return salesChannels.filter((channel) => {
const name = String(channel?.name || '').toLowerCase();
const domain = String(channel?.domain || '').toLowerCase();
return name.includes(query) || domain.includes(query);
});
}, [salesChannels, searchQuery]);
const canUseSalesChannels = salesChannelsStatus === 'success' && salesChannels.length > 0;
const isSalesChannelMode = entryMode === 'sales-channel' && canUseSalesChannels;
const effectiveSelectedSalesChannelId = filteredSalesChannels.some(
(channel) => getChannelId(channel) === selectedSalesChannelId
)
? selectedSalesChannelId
: (filteredSalesChannels[0] ? getChannelId(filteredSalesChannels[0]) : '');
const submitDisabled = status === 'loading'
|| (isSalesChannelMode ? !effectiveSelectedSalesChannelId : !url.trim());
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
if (!url.trim()) return;
const isSalesChannelMode = entryMode === 'sales-channel' && salesChannelsStatus === 'success';
if (isSalesChannelMode && !effectiveSelectedSalesChannelId) return;
if (!isSalesChannelMode && !url.trim()) return;
setStatus('loading'); setStatus('loading');
setError(''); setError('');
try { try {
const res = await apiClient.post('/api/businesses', { websiteUrl: url.trim() }); const payload = isSalesChannelMode
? { salesChannelId: effectiveSelectedSalesChannelId }
: { websiteUrl: url.trim() };
const res = await apiClient.post('/api/businesses', payload);
setBrandName(res.data.brandName); setBrandName(res.data.brandName);
setStatus('success'); setStatus('success');
} catch (err) { } catch (err) {
@ -26,7 +109,6 @@ export default function RegisterBusinessModal({ onClose }) {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm">
<div className="bg-white border border-gray-200 rounded-xl p-8 w-full max-w-md shadow-xl"> <div className="bg-white border border-gray-200 rounded-xl p-8 w-full max-w-md shadow-xl">
{/* Success */}
{status === 'success' && ( {status === 'success' && (
<div className="text-center"> <div className="text-center">
<div className="w-14 h-14 rounded-full bg-green-50 text-green-600 flex items-center justify-center mx-auto mb-4 text-2xl"></div> <div className="w-14 h-14 rounded-full bg-green-50 text-green-600 flex items-center justify-center mx-auto mb-4 text-2xl"></div>
@ -42,30 +124,111 @@ export default function RegisterBusinessModal({ onClose }) {
</div> </div>
)} )}
{/* Form */}
{(status === 'idle' || status === 'loading' || status === 'error') && ( {(status === 'idle' || status === 'loading' || status === 'error') && (
<> <>
<div className="mb-6"> <div className="mb-6">
<h2 className="text-xl font-bold text-gray-900 mb-2 tracking-tight">Add a Business</h2> <h2 className="text-xl font-bold text-gray-900 mb-2 tracking-tight">Add a Business</h2>
<p className="text-gray-500 text-sm leading-relaxed"> <p className="text-gray-500 text-sm leading-relaxed">
Enter your website URL. We'll scrape your site and extract brand context to generate TRAI-compliant SMS templates. Choose a sales channel first when available. If that lookup fails, you can still enter the website URL and we&apos;ll scrape it directly.
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div className="rounded-xl bg-gray-50 p-1 grid grid-cols-2 gap-1">
<label className="block text-sm font-semibold text-gray-700 mb-1.5 tracking-wide">Website URL</label> <button
<input type="button"
type="url" onClick={() => canUseSalesChannels && setEntryMode('sales-channel')}
value={url} disabled={!canUseSalesChannels || status === 'loading'}
onChange={e => setUrl(e.target.value)} className={`px-3 py-2 rounded-[10px] text-sm font-semibold transition ${
placeholder="https://yourstore.com" isSalesChannelMode
? 'bg-white text-gray-900 shadow-sm'
: canUseSalesChannels
? 'text-gray-500 hover:text-gray-900'
: 'text-gray-300 cursor-not-allowed'
}`}
>
Sales Channel
</button>
<button
type="button"
onClick={() => setEntryMode('manual')}
disabled={status === 'loading'} disabled={status === 'loading'}
className="w-full px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition disabled:opacity-50 text-sm shadow-sm" className={`px-3 py-2 rounded-[10px] text-sm font-semibold transition ${
required !isSalesChannelMode ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-900'
/> }`}
>
Website URL
</button>
</div> </div>
{isSalesChannelMode ? (
<div className="space-y-3">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1.5 tracking-wide">Search Sales Channels</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by name or domain"
disabled={status === 'loading' || salesChannelsStatus === 'loading'}
className="w-full px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition disabled:opacity-50 text-sm shadow-sm"
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1.5 tracking-wide">Sales Channel</label>
{filteredSalesChannels.length > 0 ? (
<select
value={effectiveSelectedSalesChannelId}
onChange={(e) => setSelectedSalesChannelId(e.target.value)}
disabled={status === 'loading'}
className="w-full px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition disabled:opacity-50 text-sm shadow-sm"
>
{filteredSalesChannels.map((channel) => (
<option key={getChannelId(channel)} value={getChannelId(channel)}>
{channel.name}{channel.domain ? `${channel.domain}` : ''}
</option>
))}
</select>
) : (
<div className="w-full px-4 py-2.5 rounded-lg bg-gray-50 border border-gray-200 text-sm text-gray-500 font-medium">
No sales channels matched your search.
</div>
)}
</div>
{salesChannelsStatus === 'loading' && (
<p className="text-xs text-gray-500 font-medium">
Loading sales channels for this company
</p>
)}
{salesChannelsError && (
<p className="text-sm text-amber-700 font-medium bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
{salesChannelsError}
</p>
)}
</div>
) : (
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1.5 tracking-wide">Website URL</label>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://yourstore.com"
disabled={status === 'loading'}
className="w-full px-4 py-2.5 rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400 font-medium focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition disabled:opacity-50 text-sm shadow-sm"
required={!isSalesChannelMode}
/>
{salesChannelsError && (
<p className="mt-2 text-xs text-gray-500 font-medium">
{salesChannelsError}
</p>
)}
</div>
)}
{status === 'error' && ( {status === 'error' && (
<p className="text-sm text-red-600 font-medium bg-red-50 border border-red-200 rounded-lg px-3 py-2">{error}</p> <p className="text-sm text-red-600 font-medium bg-red-50 border border-red-200 rounded-lg px-3 py-2">{error}</p>
)} )}
@ -81,7 +244,7 @@ export default function RegisterBusinessModal({ onClose }) {
</button> </button>
<button <button
type="submit" type="submit"
disabled={status === 'loading' || !url.trim()} disabled={submitDisabled}
className="flex-[1.2] py-2.5 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium transition shadow-sm disabled:opacity-50 flex items-center justify-center gap-2" className="flex-[1.2] py-2.5 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium transition shadow-sm disabled:opacity-50 flex items-center justify-center gap-2"
> >
{status === 'loading' ? ( {status === 'loading' ? (
@ -92,7 +255,7 @@ export default function RegisterBusinessModal({ onClose }) {
{status === 'loading' && ( {status === 'loading' && (
<p className="text-xs text-gray-500 font-medium text-center pt-2"> <p className="text-xs text-gray-500 font-medium text-center pt-2">
Scraping your site and extracting brand context. This may take 2030 seconds. Fetching the website context and extracting brand details. This may take 2030 seconds.
</p> </p>
)} )}
</form> </form>

View File

@ -37,6 +37,28 @@ function TopLevelStatus({ done, active }) {
return <span className="inline-block h-2.5 w-2.5 rounded-full bg-gray-200" />; return <span className="inline-block h-2.5 w-2.5 rounded-full bg-gray-200" />;
} }
function StageMarker({ done, active, enabled }) {
if (done) {
return (
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-indigo-600 text-white shadow-sm">
<svg className="h-2.5 w-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
</span>
);
}
if (active) {
return <span className="inline-block h-3 w-3 rounded-full border-2 border-white bg-[#8b2bfa] shadow-[0_0_0_1px_rgba(139,43,250,0.2)]" />;
}
if (!enabled) {
return <span className="inline-block h-3 w-3 rounded-full bg-gray-200" />;
}
return <span className="inline-block h-3 w-3 rounded-full bg-indigo-200" />;
}
export default function Sidebar() { export default function Sidebar() {
const { const {
activeBusiness, activeBusiness,
@ -124,70 +146,80 @@ export default function Sidebar() {
{/* Nav */} {/* Nav */}
<nav className="flex-1 px-3 pt-5"> <nav className="flex-1 px-3 pt-5">
<div className="space-y-2"> <div className="space-y-1">
{stepItems.map((item) => ( {stepItems.map((item, index) => (
<div key={item.id}> <div key={item.id} className="relative grid grid-cols-[26px_minmax(0,1fr)] gap-2">
{item.enabled ? ( <div className="relative flex min-h-full justify-center">
<NavLink {index > 0 && <div className="absolute left-1/2 -top-2 h-7 w-px -translate-x-1/2 bg-gray-200" />}
to={item.to} {index < stepItems.length - 1 && <div className="absolute left-1/2 top-5 -bottom-2 w-px -translate-x-1/2 bg-gray-200" />}
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-semibold transition-colors duration-150 ${ <div className="absolute left-1/2 top-5 -translate-x-1/2 -translate-y-1/2">
item.active <StageMarker done={item.done} active={item.active} enabled={item.enabled} />
? 'bg-gray-100/70 text-gray-900'
: 'text-gray-500 hover:text-gray-900 hover:bg-gray-50'
}`}
>
{SVG_ICONS[item.id]}
<span className="flex-1 truncate">{item.label}</span>
<div className="flex items-center gap-3">
{!item.substeps && <TopLevelStatus done={item.done} active={item.active} />}
{item.substeps && (
<svg
className={`h-4 w-4 text-gray-400 transition-transform ${item.expanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
)}
</div>
</NavLink>
) : (
<div
aria-disabled="true"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-semibold text-gray-300 cursor-not-allowed select-none"
>
{SVG_ICONS[item.id]}
<span className="flex-1 truncate">{item.label}</span>
<div className="flex items-center gap-3">
{!item.substeps && <TopLevelStatus done={item.done} active={item.active} />}
</div>
</div> </div>
)} </div>
{item.expanded && item.substeps && ( <div>
<div className="relative mt-2 mb-2 pb-1"> {item.enabled ? (
<div className="absolute left-[21.5px] top-1 bottom-3 w-px bg-gray-200" /> <NavLink
<div className="space-y-0.5"> to={item.to}
{item.substeps.map((substep) => ( className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-semibold transition-colors duration-150 ${
<div key={substep.id} className="relative flex items-center pr-3 group cursor-default"> item.active
<div className="w-[44px] flex justify-center items-center shrink-0"> ? 'bg-gray-100/70 text-gray-900'
{substep.active && <div className="w-1.5 h-1.5 rounded-full bg-[#8b2bfa] z-10 shadow-[0_0_0_2px_white]" />} : 'text-gray-500 hover:text-gray-900 hover:bg-gray-50'
</div> }`}
<div >
className={`flex-1 px-3 py-2.5 rounded-[12px] text-[14px] transition-colors ${ {SVG_ICONS[item.id]}
substep.active <span className="flex-1 truncate">{item.label}</span>
? 'bg-[#f5eeff] text-[#8b2bfa] font-semibold' <div className="flex items-center gap-3">
: 'text-gray-500 font-medium hover:text-gray-900 hover:bg-gray-50' {!item.substeps && <TopLevelStatus done={item.done} active={item.active} />}
}`} {item.substeps && (
<svg
className={`h-4 w-4 text-gray-400 transition-transform ${item.expanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
{substep.label} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</div> </svg>
</div> )}
))} </div>
</NavLink>
) : (
<div
aria-disabled="true"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-semibold text-gray-300 cursor-not-allowed select-none"
>
{SVG_ICONS[item.id]}
<span className="flex-1 truncate">{item.label}</span>
<div className="flex items-center gap-3">
{!item.substeps && <TopLevelStatus done={item.done} active={item.active} />}
</div>
</div> </div>
</div> )}
)}
{item.expanded && item.substeps && (
<div className="relative mt-2 mb-2 pb-1">
<div className="absolute left-[21.5px] top-1 bottom-3 w-px bg-gray-200" />
<div className="space-y-0.5">
{item.substeps.map((substep) => (
<div key={substep.id} className="relative flex items-center pr-3 group cursor-default">
<div className="w-[44px] flex justify-center items-center shrink-0">
{substep.active && <div className="w-1.5 h-1.5 rounded-full bg-[#8b2bfa] z-10 shadow-[0_0_0_2px_white]" />}
</div>
<div
className={`flex-1 px-3 py-2.5 rounded-[12px] text-[14px] transition-colors ${
substep.active
? 'bg-[#f5eeff] text-[#8b2bfa] font-semibold'
: 'text-gray-500 font-medium hover:text-gray-900 hover:bg-gray-50'
}`}
>
{substep.label}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div> </div>
))} ))}
</div> </div>

71
server/fdk.js Normal file
View File

@ -0,0 +1,71 @@
require('dotenv').config();
const { setupFdk } = require('@gofynd/fdk-extension-javascript/express');
const { MemoryStorage } = require('@gofynd/fdk-extension-javascript/express/storage');
function normalizeEnvText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function trimTrailingSlash(value) {
return normalizeEnvText(value).replace(/\/+$/, '');
}
function buildRedirectUrl(baseUrl, companyId) {
const normalizedBaseUrl = trimTrailingSlash(baseUrl);
const query = companyId ? `?blt-gtw-fc-cid=${encodeURIComponent(companyId)}` : '';
return `${normalizedBaseUrl}/${query}`.replace(/\/\?/, '/?');
}
function createFdkExtension() {
const apiKey = normalizeEnvText(process.env.EXTENSION_API_KEY);
const apiSecret = normalizeEnvText(process.env.EXTENSION_API_SECRET);
const baseUrl = normalizeEnvText(process.env.EXTENSION_BASE_URL || process.env.EXTENSION_URL);
const cluster = normalizeEnvText(process.env.EXTENSION_CLUSTER_URL) || 'https://api.fynd.com';
if (!apiKey || !apiSecret || !baseUrl) {
return null;
}
try {
return setupFdk({
api_key: apiKey,
api_secret: apiSecret,
base_url: trimTrailingSlash(baseUrl),
cluster,
scopes: ['company/applications/read'],
callbacks: {
auth: async (req) => buildRedirectUrl(baseUrl, req?.fdkSession?.company_id || req?.query?.company_id || ''),
uninstall: async (req) => {
const companyId = req?.body?.company_id || req?.query?.company_id || req?.fdkSession?.company_id || '';
console.log(`[FDK] uninstall callback received for company ${companyId || 'unknown'}`);
},
},
storage: new MemoryStorage('sms_extension_'),
access_mode: 'offline',
});
} catch (error) {
console.warn(`[FDK] Failed to initialize FDK: ${error.message}`);
return null;
}
}
const fdkExtension = createFdkExtension();
async function getPlatformClientForCompany(companyId) {
if (!fdkExtension) {
throw new Error('FDK is not configured');
}
if (!normalizeEnvText(companyId)) {
throw new Error('companyId is required to fetch a platform client');
}
return fdkExtension.getPlatformClient(companyId);
}
module.exports = {
fdkExtension,
isFdkConfigured: Boolean(fdkExtension),
getPlatformClientForCompany,
};

View File

@ -1,10 +1,12 @@
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const cookieParser = require('cookie-parser');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const businessesRoutes = require('./routes/businesses'); const businessesRoutes = require('./routes/businesses');
const { fdkExtension, isFdkConfigured } = require('./fdk');
const app = express(); const app = express();
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
@ -13,11 +15,21 @@ const CLIENT_DIST_DIR = [
path.join(__dirname, '..', 'client', 'dist'), path.join(__dirname, '..', 'client', 'dist'),
].find((dir) => fs.existsSync(path.join(dir, 'index.html'))); ].find((dir) => fs.existsSync(path.join(dir, 'index.html')));
app.use(cors()); app.use(cors({ origin: true, credentials: true }));
app.use(cookieParser('ext.session'));
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Health check // Health check
app.get('/api/health', (req, res) => res.json({ ok: true, timestamp: new Date().toISOString() })); app.get('/api/health', (req, res) => res.json({
ok: true,
timestamp: new Date().toISOString(),
fdkConfigured: isFdkConfigured,
}));
if (fdkExtension) {
app.use(fdkExtension.fdkHandler);
}
// Routes // Routes
app.use('/api/businesses', businessesRoutes); app.use('/api/businesses', businessesRoutes);

424
server/package-lock.json generated
View File

@ -8,8 +8,10 @@
"name": "sms-extension-server", "name": "sms-extension-server",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@gofynd/fdk-extension-javascript": "^1.1.5-beta.1",
"@pixelbin/admin": "^2.0.0", "@pixelbin/admin": "^2.0.0",
"axios": "^1.6.8", "axios": "^1.6.8",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.18.2", "express": "^4.18.2",
@ -21,6 +23,132 @@
"nodemon": "^3.1.0" "nodemon": "^3.1.0"
} }
}, },
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
"license": "MIT",
"dependencies": {
"@so-ric/colorspace": "^1.1.6",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@gofynd/fdk-client-javascript": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/@gofynd/fdk-client-javascript/-/fdk-client-javascript-3.21.0.tgz",
"integrity": "sha512-BadF2lynNAHXkr6zx6bmnqGEI+3EB+eaGd46etd9f64Z5g9RWaIrxR8rsi/iNx10nfPmzhprQDpxKaosh0QsPw==",
"license": "ISC",
"dependencies": {
"@gofynd/fp-signature": "^1.0.1",
"axios": "^1.6.4",
"camelcase": "^6.3.0",
"joi": "^17.7.0",
"loglevel": "^1.8.1",
"query-string": "^7.1.3"
}
},
"node_modules/@gofynd/fdk-client-javascript/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@gofynd/fdk-client-javascript/node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/@gofynd/fdk-client-javascript/node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@gofynd/fdk-extension-javascript": {
"version": "1.1.5-beta.1",
"resolved": "https://registry.npmjs.org/@gofynd/fdk-extension-javascript/-/fdk-extension-javascript-1.1.5-beta.1.tgz",
"integrity": "sha512-C41nKlRVSota8RKCJ3xu/KO59kDoCVCk4om3zsRp9TNBvwyCrW6xFemQP9Ohs5CQnonut7KCWaS1k9AL4Dy17g==",
"license": "ISC",
"dependencies": {
"axios": "^1.6.4",
"crypto-js": "^4.2.0",
"lodash": "^4.17.21",
"querystring": "^0.2.1",
"url-join": "^4.0.1",
"uuid": "^8.3.2",
"validator": "^13.5.2",
"winston": "^3.2.1"
},
"peerDependencies": {
"@gofynd/fdk-client-javascript": ">=1.4.13"
}
},
"node_modules/@gofynd/fdk-extension-javascript/node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/@gofynd/fdk-extension-javascript/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@gofynd/fp-signature": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@gofynd/fp-signature/-/fp-signature-1.0.2.tgz",
"integrity": "sha512-7cDq9nzfHyxAanV3g5MYcDvLwzYIdrzSz5TOpyHfneFvbrYYjkB6+7OktsY8IErHdS+TKt1Emoo9ES/C7eZZpw==",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.2.0"
}
},
"node_modules/@gofynd/fp-signature/node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -80,6 +208,16 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@so-ric/colorspace": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
"license": "MIT",
"dependencies": {
"color": "^5.0.2",
"text-hex": "1.0.x"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.19.130", "version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
@ -115,6 +253,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/abort-controller": { "node_modules/abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -178,6 +322,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -376,6 +526,52 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/color": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
"license": "MIT",
"dependencies": {
"color-convert": "^3.1.3",
"color-string": "^2.1.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/color-convert": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
"license": "MIT",
"dependencies": {
"color-name": "^2.0.0"
},
"engines": {
"node": ">=14.6"
}
},
"node_modules/color-name": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/color-string": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
"license": "MIT",
"dependencies": {
"color-name": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -433,6 +629,25 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@ -546,6 +761,12 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"license": "MIT"
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@ -670,6 +891,12 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -710,6 +937,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT"
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@ -1025,6 +1258,18 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -1044,6 +1289,54 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"license": "MIT",
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/logform/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1321,6 +1614,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"license": "MIT",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/openai": { "node_modules/openai": {
"version": "4.104.0", "version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
@ -1444,6 +1746,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/querystring": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
"integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==",
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
"license": "MIT",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -1522,6 +1834,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -1686,6 +2007,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@ -1740,6 +2070,12 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -1778,6 +2114,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"license": "MIT",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1819,6 +2164,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"license": "MIT"
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -1847,6 +2198,15 @@
"uuid": "dist-node/bin/uuid" "uuid": "dist-node/bin/uuid"
} }
}, },
"node_modules/validator": {
"version": "13.15.26",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -1881,6 +2241,70 @@
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/winston": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
"license": "MIT",
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.7.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.9.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"license": "MIT",
"dependencies": {
"logform": "^2.7.0",
"readable-stream": "^3.6.2",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/winston/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -8,8 +8,10 @@
"dev": "nodemon index.js" "dev": "nodemon index.js"
}, },
"dependencies": { "dependencies": {
"@gofynd/fdk-extension-javascript": "^1.1.5-beta.1",
"@pixelbin/admin": "^2.0.0", "@pixelbin/admin": "^2.0.0",
"axios": "^1.6.8", "axios": "^1.6.8",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.18.2", "express": "^4.18.2",

View File

@ -13,6 +13,7 @@ const {
deleteBusinessFiles, deleteBusinessFiles,
} = require('../services/pixelbin'); } = require('../services/pixelbin');
const DEFAULT_EVENTS = require('../config/defaultEvents'); const DEFAULT_EVENTS = require('../config/defaultEvents');
const { getPlatformClientForCompany } = require('../fdk');
const axios = require('axios'); const axios = require('axios');
const MERCHANT_ID = () => process.env.MERCHANT_ID; const MERCHANT_ID = () => process.env.MERCHANT_ID;
@ -25,7 +26,10 @@ function normalizeScopeId(value) {
function getCompanyId(req) { function getCompanyId(req) {
return normalizeScopeId( return normalizeScopeId(
req.get('x-company-id') req.fdkSession?.company_id
|| req.get('x-company-id')
|| req.params?.companyId
|| req.params?.company_id
|| req.query?.companyId || req.query?.companyId
|| req.query?.company_id || req.query?.company_id
|| req.body?.companyId || req.body?.companyId
@ -37,6 +41,7 @@ function getCompanyId(req) {
function getApplicationId(req) { function getApplicationId(req) {
return normalizeScopeId( return normalizeScopeId(
req.get('x-application-id') req.get('x-application-id')
|| req.body?.salesChannelId
|| req.query?.applicationId || req.query?.applicationId
|| req.query?.application_id || req.query?.application_id
|| req.body?.applicationId || req.body?.applicationId
@ -197,6 +202,135 @@ function normalizeProvider(provider = {}, fallbackUpdatedAt = null) {
}; };
} }
function normalizeWebsiteUrl(value) {
const rawValue = normalizeText(value);
if (!rawValue) return '';
const candidate = /^https?:\/\//i.test(rawValue) ? rawValue : `https://${rawValue}`;
try {
const url = new URL(candidate);
return url.toString().replace(/\/$/, '');
} catch {
return '';
}
}
function extractDomainName(domain) {
if (!domain) return '';
if (typeof domain === 'string') return normalizeText(domain);
if (typeof domain === 'object') {
return firstNonEmptyText(domain.name, domain.domain_url, domain.url, domain.host);
}
return '';
}
function rankDomain(domain) {
if (!domain || typeof domain !== 'object') return 0;
let score = 0;
if (domain.is_primary) score += 4;
if (domain.verified) score += 2;
if (!domain.is_shortlink) score += 1;
return score;
}
function pickPreferredDomain(...sources) {
const candidates = [];
for (const source of sources) {
if (!source) continue;
if (Array.isArray(source)) {
for (const item of source) {
const name = extractDomainName(item);
if (name) candidates.push({ raw: item, name });
}
continue;
}
const name = extractDomainName(source);
if (name) candidates.push({ raw: source, name });
}
if (!candidates.length) return '';
candidates.sort((left, right) => rankDomain(right.raw) - rankDomain(left.raw));
return candidates[0].name;
}
function normalizeSalesChannel(application = {}, domains = []) {
const domainName = pickPreferredDomain(application.domain, application.domains, domains);
const id = normalizeScopeId(application._id || application.id || application.application_id);
return {
id,
salesChannelId: id,
applicationId: id,
name: firstNonEmptyText(application.name, application.display_name, application.slug),
domain: domainName,
websiteUrl: normalizeWebsiteUrl(domainName),
isActive: application.is_active !== false,
logoUrl: firstNonEmptyText(
application.logo?.secure_url,
application.logo?.url,
application.logo,
application.favicon?.secure_url,
application.favicon?.url
),
};
}
async function getPlatformClient(req, companyId) {
if (req?.platformClient) return req.platformClient;
return getPlatformClientForCompany(companyId);
}
async function listAllSalesChannels(platformClient, query = '') {
const items = [];
const pageSize = 100;
let pageNo = 1;
while (true) {
const response = await platformClient.configuration.getApplications({
pageNo,
pageSize,
q: normalizeText(query) || undefined,
});
const batch = Array.isArray(response?.items) ? response.items : [];
items.push(...batch);
const totalPages = Number(response?.page?.total_page) || Number(response?.page?.total_pages) || 0;
if (!batch.length || batch.length < pageSize || (totalPages && pageNo >= totalPages)) {
break;
}
pageNo += 1;
}
return items;
}
async function getSalesChannelDetails(platformClient, salesChannelId) {
const applicationClient = platformClient.application(salesChannelId).configuration;
const [applicationResponse, domainsResponse] = await Promise.allSettled([
applicationClient.getApplicationById(),
applicationClient.getDomains(),
]);
if (applicationResponse.status !== 'fulfilled' && domainsResponse.status !== 'fulfilled') {
const error = applicationResponse.reason || domainsResponse.reason || new Error('Unable to fetch sales channel details');
throw error;
}
const application = applicationResponse.status === 'fulfilled'
? applicationResponse.value
: { _id: salesChannelId };
const domains = domainsResponse.status === 'fulfilled' ? domainsResponse.value?.domains || [] : [];
return normalizeSalesChannel(application, domains);
}
const LEGACY_DEFAULT_EVENT_SLUGS = new Set(['confirmed', 'pack', 'cancelled']); const LEGACY_DEFAULT_EVENT_SLUGS = new Set(['confirmed', 'pack', 'cancelled']);
const EVENT_TEMPLATE_FALLBACKS = { const EVENT_TEMPLATE_FALLBACKS = {
bag_confirmed: ['confirmed'], bag_confirmed: ['confirmed'],
@ -754,14 +888,91 @@ router.get('/', async (req, res) => {
} }
}); });
// POST /api/businesses — create new business from websiteUrl // GET /api/businesses/sales-channels
router.get('/sales-channels', async (req, res) => {
try {
const companyId = getCompanyId(req);
if (!companyId) {
throw createHttpError(400, 'companyId is required');
}
const platformClient = await getPlatformClient(req, companyId);
const applications = await listAllSalesChannels(platformClient);
const salesChannels = applications
.map((application) => normalizeSalesChannel(application))
.filter((channel) => channel.id && channel.name)
.sort((left, right) => left.name.localeCompare(right.name));
res.json({ salesChannels });
} catch (err) {
if (err.message === 'FDK is not configured') {
return res.status(503).json({
error: 'Sales channel fetch is unavailable',
code: 'SALES_CHANNEL_FETCH_UNAVAILABLE',
});
}
sendRouteError(res, createHttpError(
err.status || 502,
err.message || 'Failed to fetch sales channels',
err.code ? { code: err.code, details: err.details } : {}
));
}
});
// POST /api/businesses — create new business from sales channel or websiteUrl fallback
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
const { websiteUrl } = req.body;
if (!websiteUrl) return res.status(400).json({ error: 'websiteUrl is required' });
const merchantId = getCompanyId(req); const merchantId = getCompanyId(req);
const applicationId = getApplicationId(req); if (!merchantId) {
throw createHttpError(400, 'companyId is required');
}
const requestedSalesChannelId = normalizeScopeId(
req.body?.salesChannelId
|| req.body?.applicationId
|| req.body?.application_id
);
let websiteUrl = normalizeWebsiteUrl(req.body?.websiteUrl);
let salesChannel = null;
if (!requestedSalesChannelId && !websiteUrl) {
throw createHttpError(
400,
'Either salesChannelId or websiteUrl is required',
{ code: 'MISSING_BUSINESS_SOURCE' }
);
}
if (requestedSalesChannelId) {
try {
const platformClient = await getPlatformClient(req, merchantId);
salesChannel = await getSalesChannelDetails(platformClient, requestedSalesChannelId);
} catch (error) {
if (!websiteUrl) {
throw createHttpError(
error.message === 'FDK is not configured' ? 503 : 502,
'Unable to fetch sales channel details',
{
code: 'SALES_CHANNEL_DETAILS_UNAVAILABLE',
details: { salesChannelId: requestedSalesChannelId, reason: error.message },
}
);
}
}
websiteUrl = websiteUrl || salesChannel?.websiteUrl || '';
}
if (!websiteUrl) {
throw createHttpError(
422,
'A website URL could not be derived from the selected sales channel. Please enter it manually.',
{ code: 'MISSING_SALES_CHANNEL_WEBSITE' }
);
}
const applicationId = requestedSalesChannelId || getApplicationId(req);
const businesses = await getIndex(merchantId); const businesses = await getIndex(merchantId);
if (applicationId && businesses.some((business) => normalizeScopeId(business.applicationId) === applicationId)) { if (applicationId && businesses.some((business) => normalizeScopeId(business.applicationId) === applicationId)) {
@ -820,10 +1031,17 @@ router.post('/', async (req, res) => {
}); });
await saveIndex(merchantId, businesses); await saveIndex(merchantId, businesses);
res.json(contextJson); res.json({
...contextJson,
salesChannel: salesChannel ? {
salesChannelId: salesChannel.id,
name: salesChannel.name,
domain: salesChannel.domain,
} : null,
});
} catch (err) { } catch (err) {
console.error('Create business error:', err.message); console.error('Create business error:', err.message);
res.status(500).json({ error: err.message }); sendRouteError(res, err);
} }
}); });