- {SVG_ICONS[item.id]}
-
{item.label}
-
- {!item.substeps && }
-
+
+ {stepItems.map((item, index) => (
+
+
+ {index > 0 &&
}
+ {index < stepItems.length - 1 &&
}
+
+
- )}
+
- {item.expanded && item.substeps && (
-
-
-
- {item.substeps.map((substep) => (
-
-
-
+ {item.enabled ? (
+
+ {SVG_ICONS[item.id]}
+ {item.label}
+
+ {!item.substeps && }
+ {item.substeps && (
+
- {substep.label}
-
-
- ))}
+
+
+ )}
+
+
+ ) : (
+
+ {SVG_ICONS[item.id]}
+
{item.label}
+
+ {!item.substeps && }
+
-
- )}
+ )}
+
+ {item.expanded && item.substeps && (
+
+
+
+ {item.substeps.map((substep) => (
+
+
+
+ {substep.label}
+
+
+ ))}
+
+
+ )}
+
))}
diff --git a/server/fdk.js b/server/fdk.js
new file mode 100644
index 0000000..452a79a
--- /dev/null
+++ b/server/fdk.js
@@ -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,
+};
diff --git a/server/index.js b/server/index.js
index c3699e9..4d27e44 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,10 +1,12 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
+const cookieParser = require('cookie-parser');
const fs = require('fs');
const path = require('path');
const businessesRoutes = require('./routes/businesses');
+const { fdkExtension, isFdkConfigured } = require('./fdk');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -13,11 +15,21 @@ const CLIENT_DIST_DIR = [
path.join(__dirname, '..', 'client', 'dist'),
].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.urlencoded({ extended: true }));
// 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
app.use('/api/businesses', businessesRoutes);
diff --git a/server/package-lock.json b/server/package-lock.json
index ca4f975..3970eb3 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -8,8 +8,10 @@
"name": "sms-extension-server",
"version": "1.0.0",
"dependencies": {
+ "@gofynd/fdk-extension-javascript": "^1.1.5-beta.1",
"@pixelbin/admin": "^2.0.0",
"axios": "^1.6.8",
+ "cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
@@ -21,6 +23,132 @@
"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": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -80,6 +208,16 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"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": {
"version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
@@ -115,6 +253,12 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -178,6 +322,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -376,6 +526,52 @@
"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": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -433,6 +629,25 @@
"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": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@@ -546,6 +761,12 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -670,6 +891,12 @@
"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": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -710,6 +937,12 @@
"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": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -1025,6 +1258,18 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -1044,6 +1289,54 @@
"@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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1321,6 +1614,15 @@
"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": {
"version": "4.104.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
@@ -1444,6 +1746,16 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1522,6 +1834,15 @@
],
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1686,6 +2007,15 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1740,6 +2070,12 @@
"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": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1778,6 +2114,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"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": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1819,6 +2164,12 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -1847,6 +2198,15 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -1881,6 +2241,70 @@
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/server/package.json b/server/package.json
index 7c1f397..2b11c4d 100644
--- a/server/package.json
+++ b/server/package.json
@@ -8,8 +8,10 @@
"dev": "nodemon index.js"
},
"dependencies": {
+ "@gofynd/fdk-extension-javascript": "^1.1.5-beta.1",
"@pixelbin/admin": "^2.0.0",
"axios": "^1.6.8",
+ "cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
diff --git a/server/routes/businesses.js b/server/routes/businesses.js
index 01de111..fd052ff 100644
--- a/server/routes/businesses.js
+++ b/server/routes/businesses.js
@@ -13,6 +13,7 @@ const {
deleteBusinessFiles,
} = require('../services/pixelbin');
const DEFAULT_EVENTS = require('../config/defaultEvents');
+const { getPlatformClientForCompany } = require('../fdk');
const axios = require('axios');
const MERCHANT_ID = () => process.env.MERCHANT_ID;
@@ -25,7 +26,10 @@ function normalizeScopeId(value) {
function getCompanyId(req) {
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?.company_id
|| req.body?.companyId
@@ -37,6 +41,7 @@ function getCompanyId(req) {
function getApplicationId(req) {
return normalizeScopeId(
req.get('x-application-id')
+ || req.body?.salesChannelId
|| req.query?.applicationId
|| req.query?.application_id
|| 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 EVENT_TEMPLATE_FALLBACKS = {
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) => {
try {
- const { websiteUrl } = req.body;
- if (!websiteUrl) return res.status(400).json({ error: 'websiteUrl is required' });
-
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);
if (applicationId && businesses.some((business) => normalizeScopeId(business.applicationId) === applicationId)) {
@@ -820,10 +1031,17 @@ router.post('/', async (req, res) => {
});
await saveIndex(merchantId, businesses);
- res.json(contextJson);
+ res.json({
+ ...contextJson,
+ salesChannel: salesChannel ? {
+ salesChannelId: salesChannel.id,
+ name: salesChannel.name,
+ domain: salesChannel.domain,
+ } : null,
+ });
} catch (err) {
console.error('Create business error:', err.message);
- res.status(500).json({ error: err.message });
+ sendRouteError(res, err);
}
});