diff --git a/app/routes/app._index copy 4.jsx b/app/routes/app._index copy 4.jsx new file mode 100644 index 0000000..be47bf8 --- /dev/null +++ b/app/routes/app._index copy 4.jsx @@ -0,0 +1,518 @@ +import React, { useMemo, useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, useSubmit, Form } from "@remix-run/react"; +import { + Page, + Layout, + Card, + BlockStack, + Text, + InlineStack, + Image, + Divider, + Button, + Modal, + TextField, + Box, + Banner, + ChoiceList, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import data4autosLogo from "../assets/data4autos_logo.png"; // ensure this exists +import turn14DistributorLogo from "../assets/turn14-logo.png"; +import { authenticate } from "../shopify.server"; + +/* =========================== + PRICING (single source of truth) + =========================== */ +const PLAN_NAME = "Starter Sync"; +const MONTHLY_AMOUNT = 79; // USD +const ANNUAL_AMOUNT = 790; // USD (β‰ˆ 2 months off) +const TRIAL_DAYS = 14; + +/* =========================== + LOADER: check subscription + =========================== */ +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + const resp = await admin.graphql(` + query { + currentAppInstallation { + activeSubscriptions { + id + status + trialDays + createdAt + currentPeriodEnd + } + } + } + `); + + const result = await resp.json(); + const subscription = + (result && + result.data && + result.data.currentAppInstallation && + result.data.currentAppInstallation.activeSubscriptions && + result.data.currentAppInstallation.activeSubscriptions[0]) || null; + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + + console.log(`Loader subscription check: ${JSON.stringify(subscription)}`); + + + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, shop }); + } + + if (shop == "racewerksengg.myshopify.com") { + return json({ redirectToBilling: false, subscription, shop }); + } + + if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { + return json({ redirectToBilling: true, subscription, shop }); + } + + return json({ redirectToBilling: false, subscription, shop }); +}; + +/* =========================== + ACTION: create subscription (Monthly or Annual) + =========================== */ +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const form = await request.formData(); + const rawCadence = form.get("cadence"); + const cadence = rawCadence === "ANNUAL" ? "ANNUAL" : "MONTHLY"; + + const interval = cadence === "ANNUAL" ? "ANNUAL" : "EVERY_30_DAYS"; + const amount = cadence === "ANNUAL" ? ANNUAL_AMOUNT : MONTHLY_AMOUNT; + + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + + + const shopDomain = (shop || "").split(".")[0]; + + + + const origin = new URL(request.url).origin; + const returnUrl = `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`; + + + const createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "${PLAN_NAME}" + returnUrl: "${returnUrl}" + lineItems: [ + { + plan: { + appRecurringPricingDetails: { + price: { amount: ${amount}, currencyCode: USD } + interval: ${interval} + } + } + } + ] + trialDays: ${TRIAL_DAYS} + + test: false + ) { + confirmationUrl + appSubscription { id status trialDays } + userErrors { field message } + } + } + `); + + const data = await createRes.json(); + + const url = + data && data.data && data.data.appSubscriptionCreate + ? data.data.appSubscriptionCreate.confirmationUrl + : null; + + const userErrors = + (data && + data.data && + data.data.appSubscriptionCreate && + data.data.appSubscriptionCreate.userErrors) || []; + const topLevelErrors = data.errors || []; + + if (!url || userErrors.length || topLevelErrors.length) { + return json( + { + errors: [ + "Failed to create subscription.", + ...userErrors.map((e) => e.message), + ...topLevelErrors.map((e) => e.message || String(e)), + ], + }, + { status: 400 } + ); + } + + return json({ confirmationUrl: url }); +}; + +/* =========================== + PAGE + =========================== */ +export default function Index() { + const actionData = useActionData(); + const loaderData = useLoaderData(); + const submit = useSubmit(); + const [activeModal, setActiveModal] = useState(false); + + const subscription = loaderData?.subscription || null; + const shop = loaderData?.shop || ""; + + console.log(`Page ${shop} subscription check: ${JSON.stringify(subscription)}`); + + // Cadence selection for the billing action + const [cadence, setCadence] = useState("MONTHLY"); + const hasConfirmationUrl = Boolean(actionData && actionData.confirmationUrl); + const errors = (actionData && actionData.errors) || []; + + const shopDomain = (shop || "").split(".")[0]; + + const items = [ + { + icon: "βš™οΈ", + text: "Manage API settings", + link: ` /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`, + }, + ]; + + const openModal = () => setActiveModal(true); + const closeModal = () => setActiveModal(false); + + /* =========================== + Helpers & preview model + =========================== */ + const formatDate = (d) => + new Date(d).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + + // We show "preview" values while the user is picking a cadence and before they confirm billing. + // After approval + reload, we show Shopify's real values from loaderData. + const isPreview = + !subscription || loaderData?.redirectToBilling || !hasConfirmationUrl; + + // Preview trial end is TRIAL_DAYS from "now" (or from subscription.createdAt if present) + const previewBase = subscription?.createdAt ? new Date(subscription.createdAt) : new Date(); + const previewTrialEnd = new Date(previewBase); + previewTrialEnd.setDate(previewTrialEnd.getDate() + TRIAL_DAYS); + + // Top fields (readOnly) will reflect selection in preview mode + const displayStatus = subscription + ? subscription.status + : "Not active β€” will be created at checkout"; + + const displayPlan = `${PLAN_NAME} β€” ${cadence === "ANNUAL" ? "Annual" : "Monthly"}`; + + const displayNextRenewal = isPreview + ? `${formatDate(previewTrialEnd)} (after ${TRIAL_DAYS}-day trial)` + : (subscription?.currentPeriodEnd ? formatDate(subscription.currentPeriodEnd) : "N/A"); + + // Compute trial days left accurately (if TRIAL) + const trialDaysLeft = useMemo(() => { + if (!subscription?.trialDays || !subscription?.createdAt) return null; + const created = new Date(subscription.createdAt); + const trialEnd = new Date(created); + trialEnd.setDate(trialEnd.getDate() + subscription.trialDays); + const now = new Date(); + const msLeft = trialEnd.getTime() - now.getTime(); + const daysLeft = Math.ceil(msLeft / (1000 * 60 * 60 * 24)); + return Math.max(0, daysLeft); + }, [subscription?.trialDays, subscription?.createdAt]); + + return ( + + + + + + + + + Welcome to your Turn14 Dashboard + + + + Data4Autos Logo + Turn14 Distributors Logo + + + + + + + + πŸš€ 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: + + + + {items.map((item, index) => ( + + + + {item.icon} + + + + {item.text} + + + + + ))} + + + + + + + + + Need help? Contact us at{" "} + support@data4autos.com + + + + + + + + + + {/* =========================== + MODAL + =========================== */} + { + const form = document.getElementById("billing-form"); + if (form && typeof form.submit === "function") { + const hidden = document.getElementById("cadence-field"); + if (hidden) hidden.value = cadence; + submit(form); + } + }, + } + } + secondaryActions={[{ content: "Close", onAction: closeModal }]} + > +
+ {/* Keep hidden input for server action compatibility */} + + + + {errors.length > 0 && ( + +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+
+ )} + + {!hasConfirmationUrl && ( + <> + {/* ===== Top read-only fields (now preview-aware) ===== */} + + + + + + + + + + + + + + + {/* ===== Live preview of the exact price that will apply after trial ===== */} + + Your selection + + + + + + + {/* ===== Radio-style plan selector ===== */} + { + const next = Array.isArray(selected) && selected[0] ? selected[0] : "MONTHLY"; + setCadence(next); + const hidden = document.getElementById("cadence-field"); + if (hidden) hidden.value = next; + }} + allowMultiple={false} + /> + + )} + + {hasConfirmationUrl && ( + + + Click the button below to open Shopify’s billing confirmation in a + new tab. If it doesn’t open, copy the link and open it manually. + + + + + + {actionData.confirmationUrl} + + + )} +
+
+
+
+
+ ); +} diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index d94b363..2d85509 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -16,9 +16,10 @@ import { Box, Banner, ChoiceList, + Badge, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; -import data4autosLogo from "../assets/data4autos_logo.png"; // ensure this exists +import data4autosLogo from "../assets/data4autos_logo.png"; import turn14DistributorLogo from "../assets/turn14-logo.png"; import { authenticate } from "../shopify.server"; @@ -26,82 +27,161 @@ import { authenticate } from "../shopify.server"; PRICING (single source of truth) =========================== */ const PLAN_NAME = "Starter Sync"; -const MONTHLY_AMOUNT = 79; // USD -const ANNUAL_AMOUNT = 790; // USD (β‰ˆ 2 months off) +const MONTHLY_AMOUNT = 79; // USD +const ANNUAL_AMOUNT = 790; // USD const TRIAL_DAYS = 14; +const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"]; /* =========================== - LOADER: check subscription + HELPERS + =========================== */ +function formatMoney(amount, currencyCode = "USD") { + if (amount == null) return "N/A"; + return `${currencyCode} ${Number(amount).toFixed(2)}`; +} + +function getIntervalLabel(interval) { + switch (interval) { + case "ANNUAL": + return "Every 12 months"; + case "EVERY_30_DAYS": + return "Every 30 days"; + default: + return interval || "N/A"; + } +} + +function getStatusTone(status) { + switch (status) { + case "ACTIVE": + return "success"; + case "TRIAL": + return "info"; + case "CANCELLED": + case "EXPIRED": + case "DECLINED": + return "critical"; + default: + return "attention"; + } +} + +/* =========================== + LOADER: fetch real subscription details =========================== */ export const loader = async ({ request }) => { - const { admin } = await authenticate.admin(request); + const { admin, session } = await authenticate.admin(request); + const shop = session.shop; const resp = await admin.graphql(` - query { + query CurrentSubscriptionDetails { currentAppInstallation { activeSubscriptions { id + name status - trialDays + test createdAt + trialDays currentPeriodEnd + lineItems { + id + plan { + pricingDetails { + __typename + ... on AppRecurringPricing { + interval + price { + amount + currencyCode + } + } + } + } + } } } } `); const result = await resp.json(); + + const subscriptions = + result?.data?.currentAppInstallation?.activeSubscriptions || []; + const subscription = - (result && - result.data && - result.data.currentAppInstallation && - result.data.currentAppInstallation.activeSubscriptions && - result.data.currentAppInstallation.activeSubscriptions[0]) || null; + subscriptions.find((sub) => ALLOWED_STATUSES.includes(sub.status)) || + subscriptions[0] || + null; - const { session } = await authenticate.admin(request); - const shop = session.shop; + const recurringPricing = + subscription?.lineItems?.find( + (item) => + item?.plan?.pricingDetails?.__typename === "AppRecurringPricing" + )?.plan?.pricingDetails || null; - if (!subscription) { - return json({ redirectToBilling: true, subscription: null, shop }); + const isSubscribed = + !!subscription && ALLOWED_STATUSES.includes(subscription.status); + + const subscriptionDetails = subscription + ? { + id: subscription.id, + name: subscription.name || PLAN_NAME, + status: subscription.status, + test: subscription.test ?? false, + createdAt: subscription.createdAt, + trialDays: subscription.trialDays ?? 0, + currentPeriodEnd: subscription.currentPeriodEnd, + interval: recurringPricing?.interval || null, + priceAmount: recurringPricing?.price?.amount || null, + currencyCode: recurringPricing?.price?.currencyCode || "USD", + } + : null; + + console.log( + `Loader subscription details for ${shop}: ${JSON.stringify(subscriptionDetails)}` + ); + + if (shop === "racewerksengg.myshopify.com") { + return json({ + redirectToBilling: false, + shop, + isSubscribed: true, + subscription: subscriptionDetails, + allSubscriptions: subscriptions, + }); } - if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { - return json({ redirectToBilling: true, subscription, shop }); - } - - return json({ redirectToBilling: false, subscription, shop }); + return json({ + redirectToBilling: !isSubscribed, + shop, + isSubscribed, + subscription: subscriptionDetails, + allSubscriptions: subscriptions, + }); }; /* =========================== - ACTION: create subscription (Monthly or Annual) + ACTION: create subscription =========================== */ export const action = async ({ request }) => { - const { admin } = await authenticate.admin(request); + const { admin, session } = await authenticate.admin(request); const form = await request.formData(); + const rawCadence = form.get("cadence"); const cadence = rawCadence === "ANNUAL" ? "ANNUAL" : "MONTHLY"; const interval = cadence === "ANNUAL" ? "ANNUAL" : "EVERY_30_DAYS"; const amount = cadence === "ANNUAL" ? ANNUAL_AMOUNT : MONTHLY_AMOUNT; - - const { session } = await authenticate.admin(request); const shop = session.shop; - - - const shopDomain = (shop || "").split(".")[0]; - - - - const origin = new URL(request.url).origin; const returnUrl = `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`; - const createRes = await admin.graphql(` - mutation { + mutation CreateSubscription { appSubscriptionCreate( - name: "${PLAN_NAME}" + name: "${PLAN_NAME} - ${cadence === "ANNUAL" ? "Annual" : "Monthly"}" returnUrl: "${returnUrl}" lineItems: [ { @@ -114,29 +194,29 @@ export const action = async ({ request }) => { } ] trialDays: ${TRIAL_DAYS} - + replacementBehavior: STANDARD test: false ) { confirmationUrl - appSubscription { id status trialDays } - userErrors { field message } + appSubscription { + id + name + status + trialDays + } + userErrors { + field + message + } } } `); const data = await createRes.json(); - const url = - data && data.data && data.data.appSubscriptionCreate - ? data.data.appSubscriptionCreate.confirmationUrl - : null; - - const userErrors = - (data && - data.data && - data.data.appSubscriptionCreate && - data.data.appSubscriptionCreate.userErrors) || []; - const topLevelErrors = data.errors || []; + const url = data?.data?.appSubscriptionCreate?.confirmationUrl || null; + const userErrors = data?.data?.appSubscriptionCreate?.userErrors || []; + const topLevelErrors = data?.errors || []; if (!url || userErrors.length || topLevelErrors.length) { return json( @@ -162,14 +242,14 @@ export default function Index() { const loaderData = useLoaderData(); const submit = useSubmit(); const [activeModal, setActiveModal] = useState(false); + const [cadence, setCadence] = useState("MONTHLY"); const subscription = loaderData?.subscription || null; const shop = loaderData?.shop || ""; + const isSubscribed = loaderData?.isSubscribed || false; - // Cadence selection for the billing action - const [cadence, setCadence] = useState("MONTHLY"); - const hasConfirmationUrl = Boolean(actionData && actionData.confirmationUrl); - const errors = (actionData && actionData.errors) || []; + const hasConfirmationUrl = Boolean(actionData?.confirmationUrl); + const errors = actionData?.errors || []; const shopDomain = (shop || "").split(".")[0]; @@ -177,7 +257,7 @@ export default function Index() { { icon: "βš™οΈ", text: "Manage API settings", - link: ` /d4a-turn14/app/settings`, + link: `/d4a-turn14/app/settings`, }, { icon: "🏷️", @@ -199,52 +279,68 @@ export default function Index() { const openModal = () => setActiveModal(true); const closeModal = () => setActiveModal(false); - /* =========================== - Helpers & preview model - =========================== */ const formatDate = (d) => - new Date(d).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); + d + ? new Date(d).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) + : "N/A"; - // We show "preview" values while the user is picking a cadence and before they confirm billing. - // After approval + reload, we show Shopify's real values from loaderData. - const isPreview = - !subscription || loaderData?.redirectToBilling || !hasConfirmationUrl; - - // Preview trial end is TRIAL_DAYS from "now" (or from subscription.createdAt if present) const previewBase = subscription?.createdAt ? new Date(subscription.createdAt) : new Date(); const previewTrialEnd = new Date(previewBase); previewTrialEnd.setDate(previewTrialEnd.getDate() + TRIAL_DAYS); - // Top fields (readOnly) will reflect selection in preview mode - const displayStatus = subscription - ? subscription.status - : "Not active β€” will be created at checkout"; - - const displayPlan = `${PLAN_NAME} β€” ${cadence === "ANNUAL" ? "Annual" : "Monthly"}`; - - const displayNextRenewal = isPreview - ? `${formatDate(previewTrialEnd)} (after ${TRIAL_DAYS}-day trial)` - : (subscription?.currentPeriodEnd ? formatDate(subscription.currentPeriodEnd) : "N/A"); - - // Compute trial days left accurately (if TRIAL) const trialDaysLeft = useMemo(() => { if (!subscription?.trialDays || !subscription?.createdAt) return null; + if (subscription.status !== "TRIAL") return null; + const created = new Date(subscription.createdAt); const trialEnd = new Date(created); trialEnd.setDate(trialEnd.getDate() + subscription.trialDays); + const now = new Date(); const msLeft = trialEnd.getTime() - now.getTime(); const daysLeft = Math.ceil(msLeft / (1000 * 60 * 60 * 24)); + return Math.max(0, daysLeft); - }, [subscription?.trialDays, subscription?.createdAt]); + }, [subscription?.trialDays, subscription?.createdAt, subscription?.status]); + + const isPreviewMode = !isSubscribed; + + const displayPlan = isPreviewMode + ? `${PLAN_NAME} β€” ${cadence === "ANNUAL" ? "Annual" : "Monthly"}` + : subscription?.name || PLAN_NAME; + + const displayInterval = isPreviewMode + ? cadence === "ANNUAL" + ? "Every 12 months" + : "Every 30 days" + : getIntervalLabel(subscription?.interval); + + const displayPrice = isPreviewMode + ? cadence === "ANNUAL" + ? `$${ANNUAL_AMOUNT}/yr` + : `$${MONTHLY_AMOUNT}/mo` + : subscription?.priceAmount + ? `${formatMoney(subscription.priceAmount, subscription.currencyCode)}${ + subscription?.interval === "ANNUAL" ? "/yr" : "/mo" + }` + : "N/A"; + + const displayNextRenewal = isPreviewMode + ? `${formatDate(previewTrialEnd)} (after ${TRIAL_DAYS}-day trial)` + : formatDate(subscription?.currentPeriodEnd); + + const displayStatus = isPreviewMode + ? "Not active β€” will be created at checkout" + : subscription?.status || "N/A"; return ( + @@ -266,6 +362,51 @@ export default function Index() { + + + + Subscription Overview + + + {isSubscribed ? subscription?.status : "NOT SUBSCRIBED"} + + + + + + + Plan: {displayPlan} + + + Billing: {displayInterval} + + + Price: {displayPrice} + + + Next renewal / period end: {displayNextRenewal} + + + Trial:{" "} + {isSubscribed + ? `${subscription?.trialDays || 0} day(s)` + : `${TRIAL_DAYS} day free trial`} + + + Trial days left:{" "} + {trialDaysLeft != null ? `${trialDaysLeft} day(s)` : "N/A"} + + + + + + + πŸš€ Data4Autos Turn14 Integration gives you the power to sync @@ -299,13 +440,19 @@ export default function Index() { > {item.icon} + - + {item.text} @@ -325,7 +472,7 @@ export default function Index() { @@ -335,9 +482,6 @@ export default function Index() { - {/* =========================== - MODAL - =========================== */} { - const form = document.getElementById("billing-form"); - if (form && typeof form.submit === "function") { - const hidden = document.getElementById("cadence-field"); - if (hidden) hidden.value = cadence; - submit(form); + : isSubscribed + ? undefined + : { + content: + cadence === "ANNUAL" + ? `Start ${TRIAL_DAYS}-day Trial β€” $${ANNUAL_AMOUNT}/yr` + : `Start ${TRIAL_DAYS}-day Trial β€” $${MONTHLY_AMOUNT}/mo`, + onAction: () => { + const form = document.getElementById("billing-form"); + if (form) { + const hidden = document.getElementById("cadence-field"); + if (hidden) hidden.value = cadence; + submit(form, { method: "post" }); + } + }, } - }, - } } secondaryActions={[{ content: "Close", onAction: closeModal }]} >
- {/* Keep hidden input for server action compatibility */} + {errors.length > 0 && ( @@ -379,94 +525,75 @@ export default function Index() { {!hasConfirmationUrl && ( <> - {/* ===== Top read-only fields (now preview-aware) ===== */} - - - - - + + + - - - + + {subscription?.createdAt && ( + + )} + - {/* ===== Live preview of the exact price that will apply after trial ===== */} - - Your selection - - - - - + {!isSubscribed && ( + <> + + Choose your billing plan + - {/* ===== Radio-style plan selector ===== */} - { - const next = Array.isArray(selected) && selected[0] ? selected[0] : "MONTHLY"; - setCadence(next); - const hidden = document.getElementById("cadence-field"); - if (hidden) hidden.value = next; - }} - allowMultiple={false} - /> + { + const next = + Array.isArray(selected) && selected[0] + ? selected[0] + : "MONTHLY"; + setCadence(next); + const hidden = document.getElementById("cadence-field"); + if (hidden) hidden.value = next; + }} + allowMultiple={false} + /> + + )} )} @@ -474,7 +601,7 @@ export default function Index() { Click the button below to open Shopify’s billing confirmation in a - new tab. If it doesn’t open, copy the link and open it manually. + new tab. + + )} + + + + + {/* 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.brands.jsx b/app/routes/app.brands.jsx index 2601313..17da40c 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -1,5 +1,5 @@ import { json } from "@remix-run/node"; -import { useLoaderData, Form, useActionData } from "@remix-run/react"; +import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react"; import { Page, Layout, @@ -12,17 +12,16 @@ import { Toast, Frame, Text, + Banner, + InlineStack, } from "@shopify/polaris"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { TitleBar } from "@shopify/app-bridge-react"; import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; import { authenticate } from "../shopify.server"; - - - - - +const PLAN_NAME = "Starter Sync"; +const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"]; async function checkShopExists(shop) { try { @@ -30,107 +29,276 @@ async function checkShopExists(shop) { `https://backend.data4autos.com/checkisshopdataexists/${shop}` ); const data = await resp.json(); - return data.status === 1; // βœ… true if shop exists, false otherwise + return data.status === 1; } catch (err) { console.error("Error checking shop:", err); - return false; // default to false if error + return false; } } -export const loader = async ({ request }) => { -// const accessToken = await getTurn14AccessTokenFromMetafield(request); - const { admin } = await authenticate.admin(request); - const { session } = await authenticate.admin(request); +function getIntervalLabel(interval) { + switch (interval) { + case "ANNUAL": + return "Every 12 months"; + case "EVERY_30_DAYS": + return "Every 30 days"; + default: + return interval || "N/A"; + } +} + +function formatMoney(amount, currencyCode = "USD") { + if (amount == null) return "N/A"; + return `${currencyCode} ${Number(amount).toFixed(2)}`; +} + +function formatDate(date) { + if (!date) return "N/A"; + return new Date(date).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +async function getSubscriptionDetails(request) { + const { admin, session } = await authenticate.admin(request); const shop = session.shop; - var accessToken = "" - try { - accessToken = await getTurn14AccessTokenFromMetafield(request); - } catch (err) { - return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop ,err}); - console.error("Error getting Turn14 access token:", err); - // Proceeding with empty accessToken - } - - - - - // 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 { + const resp = await admin.graphql(` + query CurrentSubscriptionDetails { + currentAppInstallation { + activeSubscriptions { + id + name + status + test + createdAt + trialDays + currentPeriodEnd + lineItems { id - title + plan { + pricingDetails { + __typename + ... on AppRecurringPricing { + interval + price { + amount + currencyCode + } + } + } + } } } } } `); - const gql = await gqlRaw.json(); - const collections = gql?.data?.collections?.edges.map(e => e.node) || []; + const result = await resp.json(); + const subscriptions = + result?.data?.currentAppInstallation?.activeSubscriptions || []; + const subscription = + subscriptions.find((sub) => ALLOWED_STATUSES.includes(sub.status)) || + subscriptions[0] || + null; + const recurringPricing = + subscription?.lineItems?.find( + (item) => + item?.plan?.pricingDetails?.__typename === "AppRecurringPricing" + )?.plan?.pricingDetails || null; + const isSubscribed = + !!subscription && ALLOWED_STATUSES.includes(subscription.status); + return { + shop, + isSubscribed, + subscription: subscription + ? { + id: subscription.id, + name: subscription.name || PLAN_NAME, + status: subscription.status, + test: subscription.test ?? false, + createdAt: subscription.createdAt, + trialDays: subscription.trialDays ?? 0, + currentPeriodEnd: subscription.currentPeriodEnd, + interval: recurringPricing?.interval || null, + priceAmount: recurringPricing?.price?.amount || null, + currencyCode: recurringPricing?.price?.currencyCode || "USD", + } + : null, + }; +} - 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; +export const loader = async ({ request }) => { + console.log("πŸš€ Loader started"); + + let admin, session, shop; - let brands = []; try { - brands = JSON.parse(rawValue); + const authResult = await authenticate.admin(request); + admin = authResult.admin; + session = authResult.session; + shop = session?.shop; + console.log("βœ… Shopify auth success"); + console.log("πŸͺ Shop:", shop); } catch (err) { - console.error("❌ Failed to parse metafield value:", err); + console.error("❌ Shopify authentication failed:", err); + return json({ + brands: [], + collections: [], + selectedBrandsFromShopify: [], + shop: "", + error: "Shopify authentication failed", + isSubscribed: false, + subscription: null, + }); } + const { isSubscribed, subscription } = await getSubscriptionDetails(request); + let accessToken = ""; + try { + console.log("πŸ”‘ Fetching Turn14 access token from metafield..."); + accessToken = await getTurn14AccessTokenFromMetafield(request); + console.log("βœ… Turn14 access token received:", accessToken ? "YES" : "EMPTY"); + } catch (err) { + console.error("❌ Error getting Turn14 access token:", err); + return json({ + brands: [], + collections: [], + selectedBrandsFromShopify: [], + shop, + error: "Failed to fetch Turn14 access token", + isSubscribed, + subscription, + }); + } + let brandJson; + try { + const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + brandJson = await brandRes.json(); - return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop }); + if (!brandRes.ok) { + return json({ + brands: [], + collections: [], + selectedBrandsFromShopify: [], + shop, + error: brandJson?.error || "Failed to fetch brands", + isSubscribed, + subscription, + }); + } + } catch (err) { + console.error("❌ Exception while fetching Turn14 brands:", err); + return json({ + brands: [], + collections: [], + selectedBrandsFromShopify: [], + shop, + error: "Turn14 brands fetch crashed", + isSubscribed, + subscription, + }); + } + + let collections = []; + try { + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + } + } + } + } + `); + + const gql = await gqlRaw.json(); + collections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + } catch (err) { + console.error("❌ Error fetching Shopify collections:", err); + } + + let selectedBrands = []; + + try { + 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; + + if (rawValue) { + selectedBrands = JSON.parse(rawValue); + } + } catch (err) { + console.error("❌ Failed parsing selected_brands metafield:", err); + } + + return json({ + brands: brandJson?.data || [], + collections, + selectedBrandsFromShopify: selectedBrands, + shop, + isSubscribed, + subscription, + }); }; export const action = async ({ request }) => { + const { isSubscribed } = await getSubscriptionDetails(request); + + if (!isSubscribed) { + return json( + { + error: + "An active subscription or free trial is required to save brand collections.", + }, + { status: 403 } + ); + } + 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" + const shop = session.shop; - selectedBrands.forEach(brand => { + selectedBrands.forEach((brand) => { delete brand.pricegroups; }); - selectedOldBrands.forEach(brand => { + selectedOldBrands.forEach((brand) => { delete brand.pricegroups; }); - const resp = await fetch("https://backend.data4autos.com/managebrands", { method: "POST", headers: { @@ -150,68 +318,45 @@ export const action = async ({ request }) => { }; export default function BrandsPage() { - const { brands, collections, selectedBrandsFromShopify, shop ,err} = useLoaderData(); - console.log(err) - // console.log(`selectedBrandsFromShopify: ${JSON.stringify(selectedBrandsFromShopify)}`); + const navigate = useNavigate(); + const { + brands = [], + selectedBrandsFromShopify = [], + shop = "", + error, + isSubscribed = false, + subscription = null, + } = useLoaderData() || {}; + 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 [selectedIdsold, setSelectedIdsold] = useState([]); + const [selectedIds, setSelectedIds] = useState(() => + (selectedBrandsFromShopify ?? []).map((b) => b.id) + ); 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 || ""); - - - - - - - - const [Turn14Enabled, setTurn14Enabled] = useState(null); // null | true | false + const [Turn14Enabled, setTurn14Enabled] = useState(null); useEffect(() => { - if (!shop) { - console.log("⚠️ shop is undefined or empty"); - return; - } + if (!shop) 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) - setSelectedIdsold(selids) + setSelectedIdsold(selectedIds); }, [toastActive]); - useEffect(() => { const term = search.toLowerCase(); - setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term))); + setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term))); }, [search, brands]); useEffect(() => { @@ -235,32 +380,33 @@ export default function BrandsPage() { setPolling(false); }; - const toggleSelect = id => - setSelectedIds(prev => - prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + const toggleSelect = (id) => { + if (!isSubscribed) return; + + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] ); + }; const allFilteredSelected = filteredBrands.length > 0 && - filteredBrands.every(b => selectedIds.includes(b.id)); + filteredBrands.every((b) => selectedIds.includes(b.id)); const toggleSelectAll = () => { - const ids = filteredBrands.map(b => b.id); + if (!isSubscribed) return; + + const ids = filteredBrands.map((b) => b.id); if (allFilteredSelected) { - setSelectedIds(prev => prev.filter(id => !ids.includes(id))); + setSelectedIds((prev) => prev.filter((id) => !ids.includes(id))); } else { - setSelectedIds(prev => Array.from(new Set([...prev, ...ids]))); + setSelectedIds((prev) => Array.from(new Set([...prev, ...ids]))); } }; - var isSubmitting; - // console.log("actionData", actionData); + let isSubmitting = false; if (actionData.status) { isSubmitting = !actionData.status && !actionData.error && !actionData.processId; - } else { - isSubmitting = false; } - // console.log("isSubmitting", isSubmitting); const toastMarkup = toastActive ? ( ) : null; - const selectedBrands = brands.filter(b => selectedIds.includes(b.id)); - const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id)); - - + const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); + const selectedOldBrands = brands.filter((b) => selectedIdsold.includes(b.id)); 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` }, + { + 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 + const trialDaysLeft = useMemo(() => { + if (!subscription?.trialDays || !subscription?.createdAt) return null; + if (subscription.status !== "TRIAL") return null; + + const created = new Date(subscription.createdAt); + const trialEnd = new Date(created); + trialEnd.setDate(trialEnd.getDate() + subscription.trialDays); + + const now = new Date(); + const msLeft = trialEnd.getTime() - now.getTime(); + const daysLeft = Math.ceil(msLeft / (1000 * 60 * 60 * 24)); + + return Math.max(0, daysLeft); + }, [subscription]); + if (Turn14Enabled === false) { return ( @@ -302,7 +476,6 @@ export default function BrandsPage() { - {/* Primary actions */}
@@ -322,7 +495,6 @@ export default function BrandsPage() {
- {/* Secondary links */}
@@ -344,85 +516,161 @@ export default function BrandsPage() { ); } - - - - // 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..."} -

-
*/} - + + {!isSubscribed && ( + + +

+ This feature is available only for merchants with an active + subscription or during the free trial period. +

+
+

+ Current status:{" "} + {subscription?.status || "Not subscribed"} +

+

+ Plan: {subscription?.name || PLAN_NAME} +

+

+ Billing: {getIntervalLabel(subscription?.interval)} +

+

+ Price:{" "} + {formatMoney( + subscription?.priceAmount, + subscription?.currencyCode + )} +

+

+ Next renewal / period end:{" "} + {formatDate(subscription?.currentPeriodEnd)} +

+

+ Trial days left:{" "} + {trialDaysLeft != null ? `${trialDaysLeft} day(s)` : "N/A"} +

+
+
+ + + +
+
+
+ )} + {error && ( + + +

{error}

+
+
+ )} + + {actionData?.error && ( + + +

{actionData.error}

+
+
+ )} -
+
+
+
+ {(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 */} -
- - - {/* -
@@ -432,22 +680,20 @@ export default function BrandsPage() { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 16, - marginTop: "120px" }} > {filteredBrands.map((brand) => (
- {/* Checkbox in top-right corner */}
toggleSelect(brand.id)} + disabled={!isSubscribed} />
- {/* Brand image */}
- {/* Brand name */} -
+ +
{brand.name}
@@ -473,4 +726,4 @@ export default function BrandsPage() { ); -} +} \ No newline at end of file diff --git a/app/routes/app.managebrand copy 3.jsx b/app/routes/app.managebrand copy 3.jsx new file mode 100644 index 0000000..8d25160 --- /dev/null +++ b/app/routes/app.managebrand copy 3.jsx @@ -0,0 +1,1000 @@ +import React, { useEffect, useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, Form, useActionData } from "@remix-run/react"; +import { + Page, + Layout, + IndexTable, + Card, + Thumbnail, + TextContainer, + Spinner, + Button, + TextField, + Banner, + InlineError, + Toast, + Frame, + Select, + ProgressBar, + Checkbox, + Text, + ChoiceList, + Popover, + OptionList, +} from "@shopify/polaris"; +import { authenticate } from "../shopify.server"; +import { TitleBar } from "@shopify/app-bridge-react"; + +const styles = { + gridContainer: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', // Two equal columns + gap: '10px', // Space between items + }, + gridItem: { + display: 'flex', + flexDirection: 'column', + }, + gridFullWidthItem: { + gridColumn: 'span 2', // This takes up the full width (for Description) + display: 'flex', + flexDirection: 'column', + }, +}; + + +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 { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + + + + const { admin } = await authenticate.admin(request); + const { session } = await authenticate.admin(request); + const shop = session.shop; + + var accessToken = "" + try { + accessToken = await getTurn14AccessTokenFromMetafield(request); + } catch (err) { + return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop }); + console.error("Error getting Turn14 access token:", err); + // Proceeding with empty accessToken + } + + + + + + + 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, 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(); + const brandId = formData.get("brandId"); + const rawCount = formData.get("productCount"); + const selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]"); + const productCount = parseInt(rawCount, 10) || 10; + + const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const accessToken = await getTurn14AccessTokenFromMetafield(request); + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + const resp = await fetch("https://backend.data4autos.com/manageProducts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ + shop, + brandID: brandId, + turn14accessToken: accessToken, + productCount, + selectedProductIds + }), + }); + + console.log("Response from manageProducts:", resp.status, resp.statusText); + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + console.log("Process ID:", processId, "Status:", status); + return json({ success: true, processId, status }); +}; + +export default function ManageBrandProducts() { + const actionData = useActionData(); + const { shop, brands, accessToken } = useLoaderData(); + const [expandedBrand, setExpandedBrand] = useState(null); + const [itemsMap, setItemsMap] = useState({}); + const [loadingMap, setLoadingMap] = useState({}); + const [productCount, setProductCount] = useState("10"); + const [initialLoad, setInitialLoad] = useState(true); + const [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData?.status || ""); + const [processId, setProcessId] = useState(actionData?.processId || null); + const [progress, setProgress] = useState(0); + const [totalProducts, setTotalProducts] = useState(0); + const [processedProducts, setProcessedProducts] = useState(0); + const [currentProduct, setCurrentProduct] = useState(null); + const [results, setResults] = useState([]); + const [detail, setDetail] = useState(""); + + + + + 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); + setStatus(actionData.status || "processing"); + setToastActive(true); + } + }, [actionData]); + + const checkStatus = async () => { + setPolling(true); + try { + const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`); + const data = await response.json(); + + setStatus(data.status); + setDetail(data.detail); + setProgress(data.progress); + setTotalProducts(data.stats.total); + setProcessedProducts(data.stats.processed); + setCurrentProduct(data.current); + + if (data.results) { + setResults(data.results); + } + + // Continue polling if still processing + if (data.status !== 'done' && data.status !== 'error') { + setTimeout(checkStatus, 2000); + } else { + setPolling(false); + } + } catch (error) { + setPolling(false); + setStatus('error'); + setDetail('Failed to check status'); + console.error('Error checking status:', error); + } + }; + useEffect(() => { + let interval; + if (status?.includes("processing") && processId) { + interval = setInterval(checkStatus, 5000); + } + return () => clearInterval(interval); + }, [status, processId]); + + const toggleAllBrands = async () => { + for (const brand of brands) { + await toggleBrandItems(brand.id); + } + }; + + useEffect(() => { + if (initialLoad && brands.length > 0) { + toggleAllBrands(); + setInitialLoad(false); + } + }, [brands, initialLoad]); + + const toggleBrandItems = async (brandId) => { + const isExpanded = expandedBrand === brandId; + if (isExpanded) { + setExpandedBrand(null); + } else { + setExpandedBrand(brandId); + if (!itemsMap[brandId]) { + setLoadingMap((prev) => ({ ...prev, [brandId]: true })); + try { + const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const data = await res.json(); + const dataitems = data.items + const validItems = Array.isArray(dataitems) + ? dataitems.filter(item => item && item.id && item.attributes) + : []; + setItemsMap((prev) => ({ ...prev, [brandId]: validItems })); + } catch (err) { + console.error("Error fetching items:", err); + setItemsMap((prev) => ({ ...prev, [brandId]: [] })); + } + setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + } + } + }; + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + + + + + + + + + + const [filters, setFilters] = useState({ make: '', model: '', year: '', drive: '', baseModel: '' }); + const [filterregulatstock, setfilterregulatstock] = useState(false) + + const [isFilter_EnableZeroStock, set_isFilter_EnableZeroStock] = useState(true) + const [isFilter_IncludeLtlFreightRequired, setisFilter_IncludeLtlFreightRequired] = useState(true) + const [isFilter_Excludeclearance_item, setisFilter_Excludeclearance_item] = useState(false) + const [isFilter_Excludeair_freight_prohibited, setisFilter_Excludeair_freight_prohibited] = useState(false) + const [isFilter_IncludeProductWithNoImages, setisFilter_IncludeProductWithNoImages] = useState(true) + + const handleFilterChange = (field) => (value) => { + setFilters((prev) => ({ ...prev, [field]: value })); + }; + + + + const applyFitmentFilters = (items) => { + return items.filter((item) => { + const tags = item?.attributes?.fitmmentTags || {}; + 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 + } + + + if (!isFilter_EnableZeroStock) { + isMatch = isMatch && item?.inventoryQuantity > 0; + } + if (!isFilter_IncludeLtlFreightRequired) { + isMatch = isMatch && item?.attributes?.ltl_freight_required !== true; + } + + if (isFilter_Excludeclearance_item) { + isMatch = isMatch && item?.attributes?.clearance_item !== true; + } + if (isFilter_Excludeair_freight_prohibited) { + isMatch = isMatch && item?.attributes?.air_freight_prohibited !== true; + } + + if (!isFilter_IncludeProductWithNoImages) { + isMatch = isMatch && item?.attributes?.files && item?.attributes?.files.length > 0; + } + + + 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` }, + ]; + + + + const [popoverActive, setPopoverActive] = useState(false); + + const togglePopover = () => setPopoverActive((active) => !active); + + + const activator = ( + + ); + // If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen + if (Turn14Enabled === false) { + // Fallback if items array is not loaded yet + const safeItems = Array.isArray(items) && items.length >= 4 ? items : [ + { link: "#", icon: "πŸ”—", text: "Connect Turn14" }, + { link: "#", icon: "βš™οΈ", text: "Settings" }, + { link: "#", icon: "❓", text: "Help" }, + { link: "#", icon: "πŸ“„", text: "Documentation" } + ]; + return ( + + + + + + +
+ + + + + + ); + } + + + return ( + + + + + {/*

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

*/} + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + Brand ID
}, + { title:
Brand Name
}, + { title:
Brand Logo
}, + { title:
Action
}, + { title:
Products Count
}, + ]} + selectable={false} + > + {brands.map((brand, index) => { + + return ( + + + {brand.id} + {brand.name} + + + + + + + + + {itemsMap[brand.id]?.length || 0} + + + + ) + })} + + + + )} + + {brands.map((brand) => { + const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []); + // console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id)); + const uniqueTags = { + make: new Set(), + model: new Set(), + year: new Set(), + drive: new Set(), + baseModel: new Set(), + }; + + (itemsMap[brand.id] || []).forEach(item => { + const tags = item?.attributes?.fitmmentTags || {}; + Object.keys(uniqueTags).forEach(key => { + (tags[key] || []).forEach(val => uniqueTags[key].add(val)); + }); + }); + + return ( + + expandedBrand === brand.id && + + ( + + {processId && ( + +
+

+ Process ID: {processId} +

+ +
+

+ Status: {status || "β€”"} +

+ + {progress > 0 && ( +
+ +

+ {processedProducts} of {totalProducts} products processed + {currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`} +

+
+ )} +
+ + {status === 'done' && results.length > 0 && ( +
+

+ Results: {results.length} products processed successfully +

+
+ )} + + {status === 'error' && ( +
+ Error: {detail} +
+ )} + + +
+
+ )} + + {loadingMap[brand.id] ? ( + + ) : ( +
+
+ item.id))} + /> + +
+ setProductCount(value)} + autoComplete="off" + /> + + set_isFilter_EnableZeroStock(!isFilter_EnableZeroStock)} + /> + + { setfilterregulatstock(!filterregulatstock) }} + /> + + + + + setisFilter_IncludeLtlFreightRequired(!isFilter_IncludeLtlFreightRequired)} + /> + + setisFilter_Excludeclearance_item(!isFilter_Excludeclearance_item)} + /> + + setisFilter_Excludeair_freight_prohibited(!isFilter_Excludeair_freight_prohibited)} + /> + + setisFilter_IncludeProductWithNoImages(!isFilter_IncludeProductWithNoImages)} + /> + + + + + + +
+
+ +
+ + + + item.id))} - /> - -
- setProductCount(value)} - autoComplete="off" + + {loadingMap[brand.id] ? ( + + ) : ( +
+
+ item.id) + )} /> + - set_isFilter_EnableZeroStock(!isFilter_EnableZeroStock)} - /> - - { setfilterregulatstock(!filterregulatstock) }} - /> - - - - - setisFilter_IncludeLtlFreightRequired(!isFilter_IncludeLtlFreightRequired)} - /> - - setisFilter_Excludeclearance_item(!isFilter_Excludeclearance_item)} - /> - - setisFilter_Excludeair_freight_prohibited(!isFilter_Excludeair_freight_prohibited)} - /> - - setisFilter_IncludeProductWithNoImages(!isFilter_IncludeProductWithNoImages)} - /> - - - - + setProductCount(value)} + autoComplete="off" + /> + + set_isFilter_EnableZeroStock( + !isFilter_EnableZeroStock + ) + } + /> -
- + + setfilterregulatstock(!filterregulatstock) + } + /> -
- - - - ({ label: m, value: m, })), ]} - selected={filters.make} - allowMultiple - /> - - - - - -
-
- {filteredItems.map((item) => ( - - - - - {/* - -

Part Number: {item?.attributes?.part_number || 'N/A'}

- -

Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}

-

Price: ${item?.attributes?.price || '0.00'}

-

Description: {item?.attributes?.part_description || 'No description available'}

-

Inventory Quantity: {item?.inventoryQuantity || 'N/A'}

-

Regular Stock : {item?.attributes?.regular_stock === true ? "YES" : "NO" || 'N/A'}

-

LTL Freight Required : {item?.attributes?.ltl_freight_required === true ? "YES" : "NO" || 'N/A'}

-

Is Clearance Item : {item?.attributes?.is_clearance_item === true ? "YES" : "NO" || 'N/A'}

-

Is Air Freight Prohibited : {item?.attributes?.is_air_freight_prohibited === true ? "YES" : "NO" || 'N/A'}

-

No. Of Images : {item?.attributes?.files.length || 'N/A'}

-
-
*/} - - -
- {/* Part Number */} -
- Part Number: {item?.attributes?.part_number || 'N/A'} -
- - {/* Category & Subcategory */} -
- Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'} -
- - {/* Price */} -
- Price: ${item?.attributes?.price || '0.00'} -
- - {/* Description (1 column) */} -
- Description: {item?.attributes?.part_description || 'No description available'} -
- - {/* Inventory Quantity */} -
- Inventory Quantity: {item?.inventoryQuantity || 'N/A'} -
- - {/* Regular Stock */} -
- Regular Stock: {item?.attributes?.regular_stock === true ? "YES" : "NO" || 'N/A'} -
- - {/* LTL Freight Required */} -
- LTL Freight Required: {item?.attributes?.ltl_freight_required === true ? "YES" : "NO" || 'N/A'} -
- - {/* Clearance Item */} -
- Is Clearance Item: {item?.attributes?.is_clearance_item === true ? "YES" : "NO" || 'N/A'} -
- - {/* Air Freight Prohibited */} -
- Is Air Freight Prohibited: {item?.attributes?.is_air_freight_prohibited === true ? "YES" : "NO" || 'N/A'} -
- - {/* Number of Images */} -
- No. Of Images: {item?.attributes?.files.length || 'N/A'} -
-
-
-
-
- ))} + + + + + + + setFilters((prev) => ({ + ...prev, + make: selected, + })) + } + options={[ + { label: "All", value: "ALL" }, + ...Array.from(makes_list).map((m) => ({ + label: m, + value: m, + })), + ]} + selected={ + Array.isArray(filters.make) + ? filters.make + : filters.make + ? [filters.make] + : [] + } + allowMultiple + /> + + + + +
+ +
+ {filteredItems.map((item) => ( + + + + + + + + +
+
+ Part Number:{" "} + {item?.attributes?.part_number || "N/A"} +
+ +
+ Category:{" "} + {item?.attributes?.category || "N/A"} >{" "} + {item?.attributes?.subcategory || "N/A"} +
+ +
+ Price: $ + {item?.attributes?.price || "0.00"} +
+ +
+ Description:{" "} + {item?.attributes?.part_description || + "No description available"} +
+ +
+ Inventory Quantity:{" "} + {item?.inventoryQuantity || "N/A"} +
+ +
+ Regular Stock:{" "} + {item?.attributes?.regular_stock === true + ? "YES" + : "NO"} +
+ +
+ LTL Freight Required:{" "} + {item?.attributes?.ltl_freight_required === + true + ? "YES" + : "NO"} +
+ +
+ Is Clearance Item:{" "} + {item?.attributes?.is_clearance_item === + true + ? "YES" + : "NO"} +
+ +
+ Is Air Freight Prohibited:{" "} + {item?.attributes + ?.is_air_freight_prohibited === true + ? "YES" + : "NO"} +
+ +
+ No. Of Images:{" "} + {item?.attributes?.files?.length || "N/A"} +
+
+
+
+
+
+ ))} +
-
- )} - - - ) - ) - })} + )} + + + ) + ); + })} + {toastMarkup} diff --git a/app/routes/app.settings.jsx b/app/routes/app.settings.jsx index 2dc4b29..4b93ab9 100644 --- a/app/routes/app.settings.jsx +++ b/app/routes/app.settings.jsx @@ -28,8 +28,9 @@ const SCOPES = [ "write_products", "read_publications", "write_publications", - "read_fulfillments", - "write_fulfillments","read_locations","write_locations" + "stagedUploadsCreate", + "read_fulfillments", + "write_files,read_files,write_fulfillments", "read_locations", "write_locations" ].join(","); const REDIRECT_URI = "https://backend.data4autos.com/auth/callback"; const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; @@ -54,7 +55,7 @@ export const loader = async ({ request }) => { 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 { @@ -118,11 +119,11 @@ export const action = async ({ request }) => { } // default / legacy: connect Turn14 flow - // const clientId = formData.get("client_id"); - // const clientSecret = formData.get("client_secret"); + 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"); + // const clientId = formData.get("demo_client_id"); + // const clientSecret = formData.get("demo_client_secret"); let tokenData; try { @@ -236,18 +237,19 @@ export default function StoreCredentials() { {/* β€”β€” TURN14 FORM β€”β€” */}
- + {/* + */} @@ -255,21 +257,21 @@ export default function StoreCredentials() { - + {/* - + */} - + @@ -321,7 +323,7 @@ export default function StoreCredentials() { /> )} -
+
diff --git a/app/routes/app.settings_2508.jsx b/app/routes/app.settings_2508.jsx index dd53196..08ffcb5 100644 --- a/app/routes/app.settings_2508.jsx +++ b/app/routes/app.settings_2508.jsx @@ -29,7 +29,7 @@ const SCOPES = [ "read_publications", "write_publications", "read_fulfillments", - "write_fulfillments","read_locations","write_locations" + "write_files,read_files,write_fulfillments","read_locations","write_locations" ].join(","); const REDIRECT_URI = "https://backend.data4autos.com/auth/callback"; const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; diff --git a/app/routes/app.settings_2909.jsx b/app/routes/app.settings_2909.jsx index 7315a88..f1b35c6 100644 --- a/app/routes/app.settings_2909.jsx +++ b/app/routes/app.settings_2909.jsx @@ -29,7 +29,7 @@ const SCOPES = [ "read_publications", "write_publications", "read_fulfillments", - "write_fulfillments","read_locations","write_locations" + "write_files,read_files,write_fulfillments","read_locations","write_locations" ].join(","); const REDIRECT_URI = "https://backend.data4autos.com/auth/callback"; const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; diff --git a/app/routes/app.settings_with test creds.jsx b/app/routes/app.settings_with test creds.jsx new file mode 100644 index 0000000..2771644 --- /dev/null +++ b/app/routes/app.settings_with test creds.jsx @@ -0,0 +1,350 @@ +// app/routes/store-credentials.jsx + +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, Form } from "@remix-run/react"; +import { useEffect, useMemo, useState } from "react"; +import { + Page, + Layout, + Card, + TextField, + Button, + TextContainer, + InlineError, + Text, + BlockStack, + Box, + Select, + Banner, + InlineStack, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +const SCOPES = [ + "read_inventory", + "read_products", + "write_inventory", + "write_products", + "read_publications", + "write_publications", + "read_fulfillments", + "write_files,read_files,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(` + { + shop { + id + name + myshopifyDomain + 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 = {}; + 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, + shopDomain: data.shop.myshopifyDomain, + shopId: data.shop.id, + savedCreds: creds, + savedPricing, + }); +}; + +// ===== 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); + + // we need shop id either way + const shopResp = await admin.graphql(`{ shop { id name myshopifyDomain } }`); + const shopJson = await shopResp.json(); + const shopId = shopJson.data.shop.id; + const shopName = shopJson.data.shop.name; + const shopDomain = shopJson.data.shop.myshopifyDomain; + + 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", { + 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 }); + } + + 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 }); + } + + const stateNonce = Math.random().toString(36).slice(2); + const installUrl = + `https://${shopDomain}/admin/oauth/authorize` + + `?client_id=${CLIENT_ID}` + + `&scope=${SCOPES}` + + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + + `&state=${stateNonce}`; + + return json({ + success: true, + confirmationUrl: installUrl, + creds, + }); +}; + +// ===== COMPONENT ===== +export default function StoreCredentials() { + const { shopName, savedCreds, savedPricing, shopDomain } = 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} + + + {/* β€”β€” TURN14 FORM β€”β€” */} +
+ + + + + + + + + + + + + + + + + + +
+ + {actionData?.error && !actionData?.pricingSaved && ( + + + + )} + + {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + +

βœ… Turn14 connected successfully!

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