From 90bedec2dbffeeb86c515d7f4e48e52e6dc0a8fb Mon Sep 17 00:00:00 2001 From: Manesh Date: Fri, 29 Aug 2025 02:47:12 +0000 Subject: [PATCH] all updates for frontend baseline --- app/assets/turn14-logo.png | Bin 0 -> 4687 bytes app/routes/app.$.jsx | 234 ++++++++ app/routes/app._index.jsx | 136 +++-- app/routes/app.brands.jsx | 334 +++++++++--- app/routes/app.brands_2408.jsx | 321 +++++++++++ app/routes/app.help.jsx | 50 +- app/routes/app.jsx | 2 +- app/routes/app.managebrand.jsx | 480 ++++++++++++++--- app/routes/app.managebrand_130825.jsx | 477 +++++++++++++++++ app/routes/app.managebrand_200825.jsx | 681 ++++++++++++++++++++++++ app/routes/app.managebrand_2408.jsx | 736 ++++++++++++++++++++++++++ app/routes/app.settings copy.jsx | 311 +++++++++++ app/routes/app.settings.jsx | 348 +++++++----- app/routes/app.settings_2222.jsx | 0 app/routes/app.settings_2508.jsx | 334 ++++++++++++ app/routes/app.testing.jsx | 95 ---- shopify.app.toml | 2 +- 17 files changed, 4114 insertions(+), 427 deletions(-) create mode 100644 app/assets/turn14-logo.png create mode 100644 app/routes/app.$.jsx create mode 100644 app/routes/app.brands_2408.jsx create mode 100644 app/routes/app.managebrand_130825.jsx create mode 100644 app/routes/app.managebrand_200825.jsx create mode 100644 app/routes/app.managebrand_2408.jsx create mode 100644 app/routes/app.settings copy.jsx create mode 100644 app/routes/app.settings_2222.jsx create mode 100644 app/routes/app.settings_2508.jsx delete mode 100644 app/routes/app.testing.jsx diff --git a/app/assets/turn14-logo.png b/app/assets/turn14-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9559038cbc7136740a7a38dcba3a0f3c2cfca82d GIT binary patch literal 4687 zcmV-V60q%wP)bhb}W*3 z%zX2}M*tv8mMmGakmOCr|E&W&w~?R9@V&+9GxykzyVK_=f1aH_myUheCN!sOhU<#c zXE{=xt`z;^5L+D-`m|?!K8Lu@CrY>9-2YRa{wbN-y8r(9KXp(Ly#L45_IrU;D9tYL zB#S}2?@>O{!HsMew+My*OZj~%pJgjT(N+l=%(LShd{BNDN_9K(AfW}cgZ$T3p5?f@ z0QxEsKow5%+Tpq@8Elw<=!r4l`49m}sqNpMY5(yt!X(e7(LPHcD>P&H=$839!B$lb%tr)iF0w3k?2p^@mb;qG=pDP8^1EA^}cIJV=|Zm(kSnQkfmYXxdV%mm`b`j44)+{ zPRw`=1fmZ_@W1L>5IY+|6tdJhUK0WAs9isWMJ87Otl?*R&hHS7s`4dZMB1kKAsv

q!1P3gk{>3zbFhwrdPfr^U$mKW1>AZoikqUXwmvko+5+hq4d_Tvl1zKbX}t+p z7XhB(JH;Oj?db$mULsOSb0UH0>2r?x;vaHSeWj_Tr~B59ZNArj&$K7HVcbOOv#(65 z4Ah5({Omp6--~Xa0IKLn|6NN7K0%bq75#*zWCtDd+TwUZA{FI5jmaOE$(76U&YsYzDPwOSki3+0UHVa^90hC1;o3D*WlJwXC zg#v)h1jeG02Hg;iCPqZs&fj%Hx=krrxjG=$h*aT>8r9H`Z*ltYI1)|pa6cR`&1NJRPq+Xo?$Etr!`IpH- zmyz1fx$sm#_>z^YEwM$T3Rjfk2=9itqNxBWrH)m3XiKI&O~;|UXX?X6r1lHrUo+jS zj4+?B4xO_;%CWELmFvZ00Mes})c$i$>nBs;T0DqL)DDihiQD zBP5Wz8Z0+GqJT_$QpcijRK6yGF=(lE91i+|pq~{JFo^z8c8Dq=e90=BT}!Sg$Ch~H za@|?tM(-mcokBFq1%P9YM#Vf*wnV#O(u-C!I1k$U zBXPs`VX#4!EiuQ$wYXAsjOn%KK67r{#ubeoI&`E0I*m$PFVfbnWtQSvvTDX+OA5`2 z$`xhJszLy%YZ~e+t)C^(ju?)Kb8$tN&k&D;X)9aC$h>PTL{$0&^xd6q<^7Kqx*gia zD0SsnAbR7<6^)(RyGElvXiq^WexhmpBV(IFi_od7j!3s;)XxTZHgd5|@nh(=6L=G7 zPdlJGcPx$2r+W5IaC?J1>blmdVa!&n>`4JxE;Wt13FFKWgV-?Wf7nzY{5o+MY4vXCkC z{@nLb&%S|l3bdz`>Z2UdC}k}6J>Z@?h^{<1CX9Zrfv3)I+94`*TJN0Q+0dS3`5@Tl z%u|d=Gwq4j8Qg0TPU)zWDZSZ+krfPcJIet2={v=D1~o{S_9VHYJK~s~b81?dbvNT+us={=7K!=SJtFrWlcC+LJ%S_p!Kg2k~mB;<=&*`IiJLoB0YI z$yLB}Zkd?FwW1S~p*=~iCW+cZHWWup206MzjbVn z@Om8gF@j!D^z%yE26ddiNKl>(?db$+;+5DX(W%-J?p?<3*j0P=1*EQNJ=HikJ?J+` zN4_utps&T17EB))Kteq0tLMzXH|@!k;!{_n+t){Dp60U;?3}Z^A=;DVic?eDgBbcV0gw ZFDvATW}Nw1Qi zJ;{iWgXZKqCoc9-^+moufRs`n<(4_o;BF%+yHMC?pq;$B^_`Xt$Q{sEsHt_pVCku& z+(DcL&FHiz$rarJop8SmtOvcNos=R{AN5hH5iWse0mY^)vSWDWS1omnvAD8g03VJc zCA_AmL|H*tNhPi4;EFnfD8VUIp@5WhwzLPu;iyBk(czkqW}Cpi1?*>_Z%bFhE6u9M)q%l(b=(HyWuIQFwUr`sQ&@`D-@P4T)1JyH8z3i;rbK(vJ#jPPIoyJu&5QS*+Dff)xzeH*35`P^V6Qye zfweHwo-QVAgH;L<=^=oEUQKiYO*_D+xVXZ>F?FFuxjOogd(u|Gvk7!kY5i{yksi2! zG&R~&mm+N^G;t!r#_Ry^s=Of4y_%?GvixC9&}%GHoV>Zi{XKlW}<3z)9_;FrdZNDqNKJ!UYiB!FT! zyh#0!yOv|x5btvhW^UqG^iu^sE@##d#+WOL{|M8S9~m_VkQR_0?a2hDD~PxIj>{OG z%a0Afl}tw98%#ej%!TP~nTrwWr>dtMI!c*OR{%UP>EamVzy+kK(w>GyM5XT%k52ar zfVvp;SPuMaIi^(5=%Yos(w<`KCufTP7}&PO4)T>!9++}k&l-`A(c7zvni8P4ufV;N zks4);#t@O}{iaACEow1R{cPug8nL_9N4dsr(rJCWtsd8#O)g4zpU7Y|5>VUobS@xl zalDf00Vm(J0Jqv$6S3scImMIJC zA$K{o&k`dFXdstDe)lNQ=LR^wB2w!&+Hyq7D`Iu<0rA1qY%Y=a(y3Zv^W^UXAMtnW zxj9Sax*>%ut1AgO#OdR`qC#GWLvxDscXJ5 zW3A4~!ti(OXHIC3`rIdN{vYHxVs(C@=_%%Wy{qw(3(I=QT^9c`Ln`eH@_tVaMo?H9 z(9+O`o$}q;fC0TApSL6S)nsJP7Wa4==+j*J-g;nso*4kEYpf}Ava@W{i(yVo`dV6y zgUOlbj%kf>yu|>J-kv_+EkDmL#-CGrKEaasF>jg_NDtn?&(tb4a@fNE_^PV>Dce~n z3b{q3MO9CFR>*tI}-TtGh+Zh_Pf)0~KQ{H^u*Qc1H99Xiw%6wDFP*%E%<%23?@Ug@K_ z#}{RsHirJ`Hgqk&U-x{^R625bvBoJH%|Oa0*rGP(-Dg)&R@>0GOVka#p6c1|5LNXU z?92dwYU;FyU-#0iWK>bYFrRkakyf_y73q9Rt&f!{`EHGv??*!) zO>=VBaqmlIEVq{WcNlOa8)clj_L!~Y^Dz3RZgqH6ein}UTyx%q$>~ViQxT#*nt_y8 zO62!2px_G1rf8*A(t9u;w042;(laeOS$0GUkJ6YLi?RG@IbLdej|1&pXl<+w*}xt- zK4s7RzgIx19=IR2znD4UxCYanTjl4WeIc;=rh@lM-oKFJdZ{=FtkVg)$3g~Dv#b%{ zc?-Crb_2Lq)i9uZ)RY_(t*8b*l-j9nXa*vwtxSW94lpW|`&v#j3soR$An5%E#^em-@@Sb#M622vgat&sPcA@Akl6-~<8Dy+J4euL=u!4Q?T5lPi!aJH+U7$6(E z=B6VWT57>n=+;sq-$#T$jPe})(b4ReRsSYi)sqGVO`=&+PZ4`>t zx4v|}%^r3g&V)Oy|8e$GhCF`Z=7nOpc7;P*FTsvGXgYno=U>jd7qeQT;*@x zTBM?;Yu3oVtNg?{;(1iwYYVtt-+$=PnHRusz@h5z&fyx9j_i8o|B0Y)PsnEtW;ucj z>cAh}P{1O;*+j3g?2*Qf0Z5xD%*6xOG8J7ROAtd8n=X1)*-^r{1N>1U)KnBh2^xCa zr9_lRO3S&~!tg><)sgbfl_*;M9`^KDj>DbG~Ly;qk{K6Ytc} RC*uGB002ovPDHLkV1if67ZCse literal 0 HcmV?d00001 diff --git a/app/routes/app.$.jsx b/app/routes/app.$.jsx new file mode 100644 index 0000000..4821ee0 --- /dev/null +++ b/app/routes/app.$.jsx @@ -0,0 +1,234 @@ +// import { Page, Card, Text } from "@shopify/polaris"; + +// export default function NotFound() { +// return ( +// +// +// +// 404 - Page Not Found +// +// +// The page you are looking for does not exist. +// +// +// +// ); +// } + + +import {Page, Card, Text, Button, Box, BlockStack, InlineStack, Link} from "@shopify/polaris"; + +export default function NotFound() { + return ( + + {/* Animated gradient background */} +

+
+
+
+ + {/* Content layer */} + + + {/* Glassy card with subtle border glow */} +
+ + + + {/* Decorative SVG */} + + + + 404 — Page not found + + + The page you’re looking for doesn’t exist or may have moved. + + + {/* Actions */} + + + + + + + {/* Helpful links (optional) */} + + + Or check{" "} + Help Center{" "} + or{" "} + Contact support. + + + + + +
+
+
+ + {/* component-scoped styles */} + + + ); +} diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index e841204..c14752a 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -14,9 +14,12 @@ import { Button, Modal, TextField, + Box, + Link, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists +import turn14DistributorLogo from "../assets/turn14-logo.png"; import { authenticate } from "../shopify.server"; // Shopify server authentication import { Form } from "@remix-run/react"; @@ -45,17 +48,25 @@ export const loader = async ({ request }) => { const result = await resp.json(); const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null; + + + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + + // For new users, there's no subscription. We will show a "Not subscribed" message. if (!subscription) { - return json({ redirectToBilling: true, subscription: null }); + return json({ redirectToBilling: true, subscription: null,shop }); } // If no active or trial subscription, return redirect signal if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { - return json({ redirectToBilling: true, subscription }); + return json({ redirectToBilling: true, subscription ,shop }); } - return json({ redirectToBilling: false, subscription }); + return json({ redirectToBilling: false, subscription,shop }); }; // Action to create subscription @@ -79,7 +90,7 @@ export const action = async ({ request }) => { } ], trialDays: 7, # ✅ trialDays is a top-level argument! - test: true + test: false ) { confirmationUrl appSubscription { @@ -113,6 +124,7 @@ export default function Index() { const [activeModal, setActiveModal] = useState(false); const subscription = loaderData?.subscription; + const shop = loaderData?.shop; // useEffect(() => { // console.log("Action data:", actionData); @@ -132,6 +144,22 @@ export default function Index() { const openModal = () => setActiveModal(true); const closeModal = () => setActiveModal(false); + // const items = [ + // { icon: "⚙️", text: "Manage API settings", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/settings" }, + // { icon: "🏷️", text: "Browse and import available brands", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/brands" }, + // { icon: "📦", text: "Sync brand collections to Shopify", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/managebrand" }, + // { icon: "🔐", text: "Handle secure Turn14 login credentials", link: "https://admin.shopify.com/store/veloxautomotive/apps/d4a-turn14/app/help" }, + // ]; + + const shopDomain = (shop || "").split(".")[0];; // from the GraphQL query above + +const items = [ + { icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` }, + { icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` }, + { icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` }, + { icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` }, +]; + return ( @@ -139,52 +167,100 @@ export default function Index() { - - Data4Autos Logo - + + + {/* Centered Heading */} + Welcome to your Turn14 Dashboard - + + {/* Logos Row */} + + Data4Autos Logo + Turn14 Distributors Logo + + - - - 🚀 Data4Autos Turn14 Integration gives you the power to sync + + + 🚀 Data4Autos Turn14 Integration gives you the power to sync product brands, manage collections, and automate catalog setup directly from Turn14 to your Shopify store. + + + {/* 🔧 */} + Use the left sidebar to: + + - 🔧 Use the left sidebar to: - - - ⚙️ Manage API settings - 🏷️ Browse and import available brands - 📦 Sync brand collections to Shopify - 🔐 Handle secure Turn14 login credentials - + style={{ + display: "grid", + gridTemplateColumns: "repeat(4, 1fr)", + gap: "1rem", + }} + > + {items.map((item, index) => ( + + + + {item.icon} + + + + {item.text} + + + + + ))} + + + - + - + + {/* Status Badge */} + + Status: Connected - Shopify x Turn14 + Shopify × Turn14 + {/* Support Info */} Need help? Contact us at{" "} - support@data4autos.com + + support@data4autos.com + - + diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx index fcb8ce7..54c7138 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -11,12 +11,32 @@ import { Spinner, Toast, Frame, + Text, } from "@shopify/polaris"; import { useEffect, useState } from "react"; import { TitleBar } from "@shopify/app-bridge-react"; import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; import { authenticate } from "../shopify.server"; + + + + + + +async function checkShopExists(shop) { + try { + const resp = await fetch( + `https://backend.data4autos.com/checkisshopdataexists/${shop}` + ); + const data = await resp.json(); + return data.status === 1; // ✅ true if shop exists, false otherwise + } catch (err) { + console.error("Error checking shop:", err); + return false; // default to false if error + } +} + export const loader = async ({ request }) => { const accessToken = await getTurn14AccessTokenFromMetafield(request); const { admin } = await authenticate.admin(request); @@ -49,7 +69,37 @@ export const loader = async ({ request }) => { const gql = await gqlRaw.json(); const collections = gql?.data?.collections?.edges.map(e => e.node) || []; - return json({ brands: brandJson.data, collections }); + + + + + + + const res = await admin.graphql(`{ + shop { + metafield(namespace: "turn14", key: "selected_brands") { + value + } + } + }`); + const data = await res.json(); + const rawValue = data?.data?.shop?.metafield?.value; + + let brands = []; + try { + brands = JSON.parse(rawValue); + + } catch (err) { + console.error("❌ Failed to parse metafield value:", err); + } + + + + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop }); }; export const action = async ({ request }) => { @@ -87,16 +137,21 @@ export const action = async ({ request }) => { }; export default function BrandsPage() { - const { brands, collections } = useLoaderData(); + const { brands, collections, selectedBrandsFromShopify, shop } = useLoaderData(); + // console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`); const actionData = useActionData() || {}; const [selectedIdsold, setSelectedIdsold] = useState([]) - const [selectedIds, setSelectedIds] = useState(() => { - const titles = new Set(collections.map(c => c.title.toLowerCase())); - return brands - .filter(b => titles.has(b.name.toLowerCase())) - .map(b => b.id); - }); + // const [selectedIds, setSelectedIds] = useState(() => { + // const titles = new Set(collections.map(c => c.title.toLowerCase())); + // return brands + // .filter(b => titles.has(b.name.toLowerCase())) + // .map(b => b.id); + // }); + const [selectedIds, setSelectedIds] = useState(() => { + return selectedBrandsFromShopify.map(b => b.id); + }); + // console.log("Selected IDS : ", selectedIds) const [search, setSearch] = useState(""); const [filteredBrands, setFilteredBrands] = useState(brands); const [toastActive, setToastActive] = useState(false); @@ -104,9 +159,33 @@ export default function BrandsPage() { const [status, setStatus] = useState(actionData.status || ""); + + + + + + const [Turn14Enabled, setTurn14Enabled] = useState(null); // null | true | false + + useEffect(() => { + if (!shop) { + console.log("⚠️ shop is undefined or empty"); + return; + } + + (async () => { + const result = await checkShopExists(shop); + console.log("✅ API status result:", result, "| shop:", shop); + setTurn14Enabled(result); + })(); + }, [shop]); + + + + + useEffect(() => { const selids = selectedIds - console.log("Selected IDS : ", selids) + // console.log("Selected IDS : ", selids) setSelectedIdsold(selids) }, [toastActive]); @@ -156,13 +235,13 @@ export default function BrandsPage() { }; var isSubmitting; - console.log("actionData", actionData); + // console.log("actionData", actionData); if (actionData.status) { isSubmitting = !actionData.status && !actionData.error && !actionData.processId; } else { isSubmitting = false; } - console.log("isSubmitting", isSubmitting); + // console.log("isSubmitting", isSubmitting); const toastMarkup = toastActive ? ( selectedIds.includes(b.id)); const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id)); - console.log("123456", selectedOldBrands) + + + + const shopDomain = (shop || "").split(".")[0]; + + const items = [ + { icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` }, + { icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` }, + { icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` }, + { icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` }, + ]; + + // If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen +if (Turn14Enabled === false) { return ( - - - + + -
- - - -
+ +
+ + Turn14 isn’t connected yet + +
+ + This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection. + +
+ + {/* Primary actions */} + + +
+ + Once connected, you’ll be able to browse brands and sync collections. + +
+ + {/* Secondary links */} + +
+
+
+
+ + ); +} + + + + + // console.log("Selected Brands:", selectedBrands) + return ( + + + +
+ + Data4Autos Turn14 Brands List + +
+ +
+
+

+ Turn 14 Status:{" "} + {Turn14Enabled === true + ? "✅ Turn14 x Shopify Connected!" + : Turn14Enabled === false + ? "❌ Turn14 x Shopify Connection Doesn't Exists" + : "Checking..."} +

+
+ + + -
+
- {(actionData.processId || false) && ( -
-

- Process ID: {actionData.processId} -

-

- Status: {status || "—"} -

- -
- )} - - + {/* Left side - Search + Select All */} +
+ {(actionData?.processId || false) && ( +
+

+ Process ID: {actionData.processId} +

+

+ Status: {status || "—"} +

+ +
+ )} + + +
+ {/* Right side - Save Button */} +
+ + + {/* +
@@ -236,30 +413,39 @@ export default function BrandsPage() { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 16, + marginTop: "120px" }} > - {filteredBrands.map(brand => ( + {filteredBrands.map((brand) => ( -
- toggleSelect(brand.id)} - /> - - {brand.name} +
+ {/* Checkbox in top-right corner */} +
+ toggleSelect(brand.id)} + /> +
+ + {/* Brand image */} +
+ +
+ {/* Brand name */} +
+ {brand.name} +
))} - -
diff --git a/app/routes/app.brands_2408.jsx b/app/routes/app.brands_2408.jsx new file mode 100644 index 0000000..530d17f --- /dev/null +++ b/app/routes/app.brands_2408.jsx @@ -0,0 +1,321 @@ +import { json } from "@remix-run/node"; +import { useLoaderData, Form, useActionData } from "@remix-run/react"; +import { + Page, + Layout, + Card, + TextField, + Checkbox, + Button, + Thumbnail, + Spinner, + Toast, + Frame, + Text, +} from "@shopify/polaris"; +import { useEffect, useState } from "react"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + const accessToken = await getTurn14AccessTokenFromMetafield(request); + const { admin } = await authenticate.admin(request); + + // fetch brands + const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const brandJson = await brandRes.json(); + if (!brandRes.ok) { + return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 }); + } + + // fetch Shopify collections + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + } + } + } + } + `); + const gql = await gqlRaw.json(); + const collections = gql?.data?.collections?.edges.map(e => e.node) || []; + + + + + + + + const res = await admin.graphql(`{ + shop { + metafield(namespace: "turn14", key: "selected_brands") { + value + } + } + }`); + const data = await res.json(); + const rawValue = data?.data?.shop?.metafield?.value; + + let brands = []; + try { + brands = JSON.parse(rawValue); + + } catch (err) { + console.error("❌ Failed to parse metafield value:", err); + } + + + + + + return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [] }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); + const selectedOldBrands = JSON.parse(formData.get("selectedOldBrands") || "[]"); + const { session } = await authenticate.admin(request); + const shop = session.shop; // "veloxautomotive.myshopify.com" + + selectedBrands.forEach(brand => { + delete brand.pricegroups; + }); + + selectedOldBrands.forEach(brand => { + delete brand.pricegroups; + }); + + + const resp = await fetch("https://backend.data4autos.com/managebrands", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ shop, selectedBrands, selectedOldBrands }), + }); + + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + return json({ processId, status }); +}; + +export default function BrandsPage() { + const { brands, collections, selectedBrandsFromShopify } = useLoaderData(); + console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`); + const actionData = useActionData() || {}; + const [selectedIdsold, setSelectedIdsold] = useState([]) + // const [selectedIds, setSelectedIds] = useState(() => { + // const titles = new Set(collections.map(c => c.title.toLowerCase())); + // return brands + // .filter(b => titles.has(b.name.toLowerCase())) + // .map(b => b.id); + // }); + + const [selectedIds, setSelectedIds] = useState(() => { + return selectedBrandsFromShopify.map(b => b.id); + }); + console.log("Selected IDS : ", selectedIds) + const [search, setSearch] = useState(""); + const [filteredBrands, setFilteredBrands] = useState(brands); + const [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData.status || ""); + + + useEffect(() => { + const selids = selectedIds + console.log("Selected IDS : ", selids) + setSelectedIdsold(selids) + }, [toastActive]); + + + useEffect(() => { + const term = search.toLowerCase(); + setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term))); + }, [search, brands]); + + useEffect(() => { + if (actionData.status) { + setStatus(actionData.status); + setToastActive(true); + } + }, [actionData.status]); + + const checkStatus = async () => { + if (!actionData.processId) return; + setPolling(true); + const resp = await fetch( + `https://backend.data4autos.com/managebrands/status/${actionData.processId}`, + { headers: { "shop-domain": window.shopify.shop || "" } } + ); + const jsonBody = await resp.json(); + setStatus( + jsonBody.status + (jsonBody.detail ? ` (${jsonBody.detail})` : "") + ); + setPolling(false); + }; + + const toggleSelect = id => + setSelectedIds(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + + const allFilteredSelected = + filteredBrands.length > 0 && + filteredBrands.every(b => selectedIds.includes(b.id)); + + const toggleSelectAll = () => { + const ids = filteredBrands.map(b => b.id); + if (allFilteredSelected) { + setSelectedIds(prev => prev.filter(id => !ids.includes(id))); + } else { + setSelectedIds(prev => Array.from(new Set([...prev, ...ids]))); + } + }; + + var isSubmitting; + console.log("actionData", actionData); + if (actionData.status) { + isSubmitting = !actionData.status && !actionData.error && !actionData.processId; + } else { + isSubmitting = false; + } + console.log("isSubmitting", isSubmitting); + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + const selectedBrands = brands.filter(b => selectedIds.includes(b.id)); + const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id)); + console.log("Selected Brands:", selectedBrands) + return ( + + + +
+ + Data4Autos Turn14 Brands List + +
+ + +
+ + {/* Left side - Search + Select All */} +
+ {(actionData?.processId || false) && ( +
+

+ Process ID: {actionData.processId} +

+

+ Status: {status || "—"} +

+ +
+ )} + + + +
+ {/* Right side - Save Button */} +
+ + + {/* +
+
+
+ + +
+ {filteredBrands.map((brand) => ( + +
+ {/* Checkbox in top-right corner */} +
+ toggleSelect(brand.id)} + /> +
+ + {/* Brand image */} +
+ +
+ {/* Brand name */} +
+ {brand.name} +
+
+
+ ))} +
+
+
+ + {toastMarkup} +
+ + ); +} diff --git a/app/routes/app.help.jsx b/app/routes/app.help.jsx index 65b5c1e..25369bd 100644 --- a/app/routes/app.help.jsx +++ b/app/routes/app.help.jsx @@ -63,23 +63,53 @@ export default function HelpPage() { {faqs.map((faq, index) => ( -
- - - - {faq.content} + + {faq.title} + + ▶ + +
+ + {/* Collapsible Body */} + +
+ + {faq.content} + +
))} + Still have questions? Email us at{" "} diff --git a/app/routes/app.jsx b/app/routes/app.jsx index 61a82c3..47d2ab9 100644 --- a/app/routes/app.jsx +++ b/app/routes/app.jsx @@ -63,7 +63,7 @@ export default function App() { 🏷️ Brands 📦 Manage Brands 🆘 Help - 🆘 Testing + {/* 🆘 Testing */} diff --git a/app/routes/app.managebrand.jsx b/app/routes/app.managebrand.jsx index d876521..612df03 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -17,10 +17,28 @@ import { Frame, Select, ProgressBar, + Checkbox, + Text, } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; import { TitleBar } from "@shopify/app-bridge-react"; + +async function checkShopExists(shop) { + try { + const resp = await fetch( + `https://backend.data4autos.com/checkisshopdataexists/${shop}` + ); + const data = await resp.json(); + return data.status === 1; // ✅ true if shop exists, false otherwise + } catch (err) { + console.error("Error checking shop:", err); + return false; // default to false if error + } +} + + + export const loader = async ({ request }) => { const { admin } = await authenticate.admin(request); const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); @@ -43,9 +61,166 @@ export const loader = async ({ request }) => { console.error("❌ Failed to parse metafield value:", err); } - return json({ brands, accessToken }); + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + return json({ brands, accessToken, shop }); }; + +const makes_list_raw = [ + 'Alfa Romeo', + 'Ferrari', + 'Dodge', + 'Subaru', + 'Toyota', + 'Volkswagen', + 'Volvo', + 'Audi', + 'BMW', + 'Buick', + 'Cadillac', + 'Chevrolet', + 'Chrysler', + 'CX Automotive', + 'Nissan', + 'Ford', + 'Hyundai', + 'Infiniti', + 'Lexus', + 'Mercury', + 'Mazda', + 'Oldsmobile', + 'Plymouth', + 'Pontiac', + 'Rolls-Royce', + 'Eagle', + 'Lincoln', + 'Mercedes-Benz', + 'GMC', + 'Saab', + 'Honda', + 'Saturn', + 'Mitsubishi', + 'Isuzu', + 'Jeep', + 'AM General', + 'Geo', + 'Suzuki', + 'E. P. Dutton, Inc.', + 'Land Rover', + 'PAS, Inc', + 'Acura', + 'Jaguar', + 'Lotus', + 'Grumman Olson', + 'Porsche', + 'American Motors Corporation', + 'Kia', + 'Lamborghini', + 'Panoz Auto-Development', + 'Maserati', + 'Saleen', + 'Aston Martin', + 'Dabryan Coach Builders Inc', + 'Federal Coach', + 'Vector', + 'Bentley', + 'Daewoo', + 'Qvale', + 'Roush Performance', + 'Autokraft Limited', + 'Bertone', + 'Panther Car Company Limited', + 'Texas Coach Company', + 'TVR Engineering Ltd', + 'Morgan', + 'MINI', + 'Yugo', + 'BMW Alpina', + 'Renault', + 'Bitter Gmbh and Co. Kg', + 'Scion', + 'Maybach', + 'Lambda Control Systems', + 'Merkur', + 'Peugeot', + 'Spyker', + 'London Coach Co Inc', + 'Hummer', + 'Bugatti', + 'Pininfarina', + 'Shelby', + 'Saleen Performance', + 'smart', + 'Tecstar, LP', + 'Kenyon Corporation Of America', + 'Avanti Motor Corporation', + 'Bill Dovell Motor Car Company', + 'Import Foreign Auto Sales Inc', + 'S and S Coach Company E.p. Dutton', + 'Superior Coaches Div E.p. Dutton', + 'Vixen Motor Company', + 'Volga Associated Automobile', + 'Wallace Environmental', + 'Import Trade Services', + 'J.K. Motors', + 'Panos', + 'Quantum Technologies', + 'London Taxi', + 'Red Shift Ltd.', + 'Ruf Automobile Gmbh', + 'Excalibur Autos', + 'Mahindra', + 'VPG', + 'Fiat', + 'Sterling', + 'Azure Dynamics', + 'McLaren Automotive', + 'Ram', + 'CODA Automotive', + 'Fisker', + 'Tesla', + 'Mcevoy Motors', + 'BYD', + 'ASC Incorporated', + 'SRT', + 'CCC Engineering', + 'Mobility Ventures LLC', + 'Pagani', + 'Genesis', + 'Karma', + 'Koenigsegg', + 'Aurora Cars Ltd', + 'RUF Automobile', + 'Dacia', + 'STI', + 'Daihatsu', + 'Polestar', + 'Kandi', + 'Rivian', + 'Lucid', + 'JBA Motorcars, Inc.', + 'Lordstown', + 'Vinfast', + 'INEOS Automotive', + 'Bugatti Rimac', + 'Grumman Allied Industries', + 'Environmental Rsch and Devp Corp', + 'Evans Automobiles', + 'Laforza Automobile Inc', + 'General Motors', + 'Consulier Industries Inc', + 'Goldacre', + 'Isis Imports Ltd', + 'PAS Inc - GMC' +]; + + + +const makes_list = makes_list_raw.sort(); + export const action = async ({ request }) => { const { admin } = await authenticate.admin(request); const formData = await request.formData(); @@ -88,7 +263,7 @@ export const action = async ({ request }) => { export default function ManageBrandProducts() { const actionData = useActionData(); - const { brands, accessToken } = useLoaderData(); + const { shop, brands, accessToken } = useLoaderData(); const [expandedBrand, setExpandedBrand] = useState(null); const [itemsMap, setItemsMap] = useState({}); const [loadingMap, setLoadingMap] = useState({}); @@ -105,6 +280,27 @@ export default function ManageBrandProducts() { const [results, setResults] = useState([]); const [detail, setDetail] = useState(""); + const [filterregulatstock, setfilterregulatstock] = useState(false) + + + + const [Turn14Enabled, setTurn14Enabled] = useState("12345"); // null | true | false + + useEffect(() => { + if (!shop) { + console.log("⚠️ shop is undefined or empty"); + return; + } + + (async () => { + const result = await checkShopExists(shop); + console.log("✅ API status result:", result, "| shop:", shop); + setTurn14Enabled(result); + })(); + }, [shop]); + + + useEffect(() => { if (actionData?.processId) { setProcessId(actionData.processId); @@ -218,28 +414,133 @@ export default function ManageBrandProducts() { setFilters((prev) => ({ ...prev, [field]: value })); }; + + const applyFitmentFilters = (items) => { return items.filter((item) => { const tags = item?.attributes?.fitmmentTags || {}; - return ( - (!filters.make || tags.make?.includes(filters.make)) && - (!filters.model || tags.model?.includes(filters.model)) && - (!filters.year || tags.year?.includes(filters.year)) && - (!filters.drive || tags.drive?.includes(filters.drive)) && - (!filters.baseModel || tags.baseModel?.includes(filters.baseModel)) - ); + const productName = item?.attributes?.product_name || ''; + const brand = item?.attributes?.brand || ''; + const partDescription = item?.attributes?.part_description || ''; + const descriptions = item?.attributes?.descriptions || []; + + const makeMatch = !filters.make || tags.make?.includes(filters.make) || productName.includes(filters.make) || brand.includes(filters.make) || descriptions.some((desc) => desc.description.includes(filters.make)); + + + const modelMatch = !filters.model || tags.model?.includes(filters.model) || productName.includes(filters.model) || brand.includes(filters.model) || descriptions.some((desc) => desc.description.includes(filters.model)); + // console.log(`Model check result: ${modelMatch}`); + + const yearMatch = !filters.year || tags.year?.includes(filters.year) || productName.includes(filters.year) || brand.includes(filters.year) || descriptions.some((desc) => desc.description.includes(filters.year)); + /// console.log(`Year check result: ${yearMatch}`); + + const driveMatch = !filters.drive || tags.drive?.includes(filters.drive) || productName.includes(filters.drive) || brand.includes(filters.drive) || descriptions.some((desc) => desc.description.includes(filters.drive)); + // console.log(`Drive check result: ${driveMatch}`); + + const baseModelMatch = !filters.baseModel || tags.baseModel?.includes(filters.baseModel) || productName.includes(filters.baseModel) || brand.includes(filters.baseModel) || descriptions.some((desc) => desc.description.includes(filters.baseModel)); + // console.log(`Base Model check result: ${baseModelMatch}`); + + // Combine all the conditions + var isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch// && item.attributes.regular_stock + if (filterregulatstock) { + isMatch = isMatch && item?.attributes?.regular_stock + } + + return isMatch; }); }; + + const selectedProductIds = [] + + const shopDomain = (shop || "").split(".")[0]; + + const items = [ + { icon: "⚙️", text: "Manage API settings", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings` }, + { icon: "🏷️", text: "Browse and import available brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands` }, + { icon: "📦", text: "Sync brand collections to Shopify", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand` }, + { icon: "🔐", text: "Handle secure Turn14 login credentials", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help` }, + ]; + + // If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen + if (Turn14Enabled === false) { + return ( + + + + + + +
+ + Turn14 isn’t connected yet + +
+ + This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection. + +
+ + {/* Primary actions */} + + +
+ + Once connected, you’ll be able to browse brands and sync collections. + +
+ + {/* Secondary links */} + +
+
+
+
+
+ + ); + } + + return ( - + +

+ Turn 14 Status:{" "} + {Turn14Enabled === true + ? "✅ Turn14 x Shopify Connected!" + : Turn14Enabled === false + ? "❌ Turn14 x Shopify Connection Doesn't Exists" + : "Checking..."} +

+ {brands.length === 0 ? ( @@ -253,10 +554,11 @@ export default function ManageBrandProducts() { resourceName={{ singular: "brand", plural: "brands" }} itemCount={brands.length} headings={[ - { title: "Brand ID" }, - { title: "Logo" }, - { title: "Action" }, - { title: "Products Count" }, + { title:
Brand ID
}, + { title:
Brand Name
}, + { title:
Brand Logo
}, + { title:
Action
}, + { title:
Products Count
}, ]} selectable={false} > @@ -266,6 +568,7 @@ export default function ManageBrandProducts() { {brand.id} + {brand.name} - - {itemsMap[brand.id]?.length || 0} + + + {itemsMap[brand.id]?.length || 0} + + ) })} @@ -292,7 +611,7 @@ export default function ManageBrandProducts() { {brands.map((brand) => { const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []); - console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id)); + // console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id)); const uniqueTags = { make: new Set(), model: new Set(), @@ -314,8 +633,8 @@ export default function ManageBrandProducts() { ( - - {processId && ( + {processId && ( +

Process ID: {processId} @@ -359,15 +678,14 @@ export default function ManageBrandProducts() {

- )} -
- +
+ )} {loadingMap[brand.id] ? ( @@ -380,63 +698,79 @@ export default function ManageBrandProducts() { value={JSON.stringify(filteredItems.map((item) => item.id))} /> - setProductCount(value)} - autoComplete="off" - /> - +
+ setProductCount(value)} + autoComplete="off" + /> + + { setfilterregulatstock(!filterregulatstock) }} + /> + + + +
- - - - item.id))} + /> + +
+ setProductCount(value)} + autoComplete="off" + /> + +
+ + +
+ + + + item.id))} + /> + +
+ setProductCount(value)} + autoComplete="off" + /> + +
+ + +
+ + + + item.id))} + /> + +
+ setProductCount(value)} + autoComplete="off" + /> + + { setfilterregulatstock(!filterregulatstock) }} + /> + + + +
+ + +
+ + + + + + + + + + + + + + + + + + {actionData?.error && ( + + + + )} + + {connected && ( + +

✅ Turn14 connected successfully!

+ + {/* —— SHOPIFY INSTALL FORM —— */} + {/*
+ + +
+ +
+
*/} +
+ )} +
+ +
+
+
+ + ); +} diff --git a/app/routes/app.settings.jsx b/app/routes/app.settings.jsx index e4217a6..7315a88 100644 --- a/app/routes/app.settings.jsx +++ b/app/routes/app.settings.jsx @@ -1,8 +1,8 @@ // app/routes/store-credentials.jsx -import { json, redirect } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { useLoaderData, useActionData, Form } from "@remix-run/react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Page, Layout, @@ -11,6 +11,12 @@ import { Button, TextContainer, InlineError, + Text, + BlockStack, + Box, + Select, + Banner, + InlineStack, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import { authenticate } from "../shopify.server"; @@ -22,10 +28,13 @@ const SCOPES = [ "write_products", "read_publications", "write_publications", + "read_fulfillments", + "write_fulfillments","read_locations","write_locations" ].join(","); const REDIRECT_URI = "https://backend.data4autos.com/auth/callback"; const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; +// ===== LOADER ===== export const loader = async ({ request }) => { const { admin } = await authenticate.admin(request); const resp = await admin.graphql(` @@ -34,111 +43,84 @@ export const loader = async ({ request }) => { id name metafield(namespace: "turn14", key: "credentials") { value } + pricing: metafield(namespace: "turn14", key: "pricing_config") { value } } } `); const { data } = await resp.json(); + let creds = {}; if (data.shop.metafield?.value) { try { creds = JSON.parse(data.shop.metafield.value); } catch { } } - //creds = {}; + creds = {}; + let savedPricing = { priceType: "map", percentage: 0 }; + if (data.shop.pricing?.value) { + try { + const p = JSON.parse(data.shop.pricing.value); + savedPricing.priceType = (p.priceType || "map").toLowerCase(); + savedPricing.percentage = Number(p.percentage) || 0; + } catch { } + } + return json({ shopName: data.shop.name, shopId: data.shop.id, savedCreds: creds, + savedPricing, }); }; -// export const action = async ({ request }) => { -// const formData = await request.formData(); -// const { admin } = await authenticate.admin(request); - -// // ——— Handle Shopify-install trigger ——— -// if (formData.get("install_shopify") === "1") { -// const shopName = formData.get("shop_name"); -// const stateNonce = Math.random().toString(36).slice(2); -// const installUrl = -// `https://${shopName}.myshopify.com/admin/oauth/authorize` + -// `?client_id=${CLIENT_ID}` + -// `&scope=${SCOPES}` + -// `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + -// `&state=${stateNonce}` + -// `&grant_options%5B%5D=per-user`; - -// // return the URL instead of redirecting -// return json({ confirmationUrl: installUrl }); -// } - - -// // ——— Otherwise handle Turn14 token exchange ——— -// const clientId = formData.get("client_id"); -// const clientSecret = formData.get("client_secret"); -// const shopInfo = await admin.graphql(`{ shop { id } }`); -// const shopId = (await shopInfo.json()).data.shop.id; - -// let tokenData; -// try { -// const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { -// method: "POST", -// headers: { "Content-Type": "application/json" }, -// body: JSON.stringify({ -// grant_type: "client_credentials", -// client_id: clientId, -// client_secret: clientSecret, -// }), -// }); -// tokenData = await tokenRes.json(); -// if (!tokenRes.ok) { -// throw new Error(tokenData.error || "Failed to fetch Turn14 token"); -// } -// } catch (err) { -// return json({ success: false, error: err.message }); -// } - -// // upsert as Shopify metafield -// const creds = { -// clientId, -// clientSecret, -// accessToken: tokenData.access_token, -// expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), -// }; -// const mutation = ` -// mutation { -// metafieldsSet(metafields: [{ -// ownerId: "${shopId}", -// namespace: "turn14", -// key: "credentials", -// type: "json", -// value: "${JSON.stringify(creds).replace(/"/g, '\\"')}" -// }]) { -// userErrors { message } -// } -// } -// `; -// const saveRes = await admin.graphql(mutation); -// const saveJson = await saveRes.json(); -// const errs = saveJson.data.metafieldsSet.userErrors; -// if (errs.length) { -// return json({ success: false, error: errs[0].message }); -// } - -// return json({ success: true, creds }); -// }; - - +// ===== ACTION ===== export const action = async ({ request }) => { const formData = await request.formData(); + const intent = formData.get("intent"); // "connect_turn14" | "save_pricing" const { admin } = await authenticate.admin(request); - // ——— Turn14 token exchange ——— - const clientId = formData.get("client_id"); - const clientSecret = formData.get("client_secret"); + // we need shop id either way const shopResp = await admin.graphql(`{ shop { id name } }`); const shopJson = await shopResp.json(); const shopId = shopJson.data.shop.id; const shopName = shopJson.data.shop.name; + if (intent === "save_pricing") { + // --- save pricing_config metafield directly --- + const priceTypeRaw = (formData.get("price_type") || "map").toString().toLowerCase(); + const percentageRaw = Number(formData.get("percentage") || 0); + + const priceType = ["map", "percentage"].includes(priceTypeRaw) ? priceTypeRaw : "map"; + const percentage = Number.isFinite(percentageRaw) ? percentageRaw : 0; + + const cfg = { priceType, percentage }; + const mutation = ` + mutation { + metafieldsSet(metafields: [{ + ownerId: "${shopId}", + namespace: "turn14", + key: "pricing_config", + type: "json", + value: "${JSON.stringify(cfg).replace(/"/g, '\\"')}" + }]) { + userErrors { message } + } + } + `; + const saveRes = await admin.graphql(mutation); + const saveJson = await saveRes.json(); + const errs = saveJson.data.metafieldsSet.userErrors; + if (errs.length) { + return json({ success: false, pricingSaved: false, error: errs[0].message }); + } + return json({ success: true, pricingSaved: true, savedPricing: cfg }); + } + + // default / legacy: connect Turn14 flow + // const clientId = formData.get("client_id"); + // const clientSecret = formData.get("client_secret"); + + const clientId = formData.get("demo_client_id"); + const clientSecret = formData.get("demo_client_secret"); + let tokenData; try { const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { @@ -158,7 +140,6 @@ export const action = async ({ request }) => { return json({ success: false, error: err.message }); } - // ——— Upsert to Shopify metafield ——— const creds = { clientId, clientSecret, @@ -185,99 +166,180 @@ export const action = async ({ request }) => { return json({ success: false, error: errs[0].message }); } - // ——— Build the Shopify OAuth URL and return it ——— const stateNonce = Math.random().toString(36).slice(2); const installUrl = `https://${shopName}.myshopify.com/admin/oauth/authorize` + `?client_id=${CLIENT_ID}` + `&scope=${SCOPES}` + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + - `&state=${stateNonce}` - //+ `&grant_options%5B%5D=per-user`; + `&state=${stateNonce}`; return json({ success: true, confirmationUrl: installUrl, + creds, }); }; - - +// ===== COMPONENT ===== export default function StoreCredentials() { - const { shopName, shopId, savedCreds } = useLoaderData(); + const { shopName, savedCreds, savedPricing } = useLoaderData(); const actionData = useActionData(); - + // open Shopify install after Connect Turn14 useEffect(() => { if (actionData?.confirmationUrl) { window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); } }, [actionData?.confirmationUrl]); - const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || ""); const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || ""); const connected = actionData?.success || Boolean(savedCreds.accessToken); + // Pricing UI state (seed from loader or last action) + const initialPriceType = useMemo( + () => (actionData?.savedPricing?.priceType || savedPricing?.priceType || "map"), + [actionData?.savedPricing?.priceType, savedPricing?.priceType] + ); + const initialPercentage = useMemo( + () => Number(actionData?.savedPricing?.percentage ?? savedPricing?.percentage ?? 0), + [actionData?.savedPricing?.percentage, savedPricing?.percentage] + ); + + const [priceType, setPriceType] = useState(initialPriceType); + const [percentage, setPercentage] = useState(initialPercentage); + + const pricingSavedOk = actionData?.pricingSaved && actionData?.success && !actionData?.error; + const pricingError = actionData?.pricingSaved === false ? actionData?.error : null; + return ( - + +
+ Data4Autos Turn14 Integration +
+ - - -

Shop: {shopName}

-
+
+ + + + + Shop: {shopName} + - {/* —— TURN14 FORM —— */} -
- - - -
- -
- + {/* —— TURN14 FORM —— */} +
+ + + + + + + + + + + + + - {actionData?.error && ( - - - - )} + + + + +
- {connected && ( - -

✅ Turn14 connected successfully!

+ {actionData?.error && !actionData?.pricingSaved && ( + + + + )} - {/* —— SHOPIFY INSTALL FORM —— */} - {/*
- - -
- -
-
*/} -
- )} -
+ {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + +

✅ Turn14 connected successfully!

+
+ )} + + {/* —— PRICING CONFIG (direct save via this route) —— */} + {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + + +
+ + + + + + + + + + + + + + +
+ + {actionData?.error && !actionData?.pricingSaved && ( + + + + )} + + {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + +

✅ Turn14 connected successfully!

+
+ )} + + {/* —— PRICING CONFIG (direct save via this route) —— */} + {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + + +
+ + +