From 688855fe48401a4ebd902c90cf6ba51e9cb6ca6e Mon Sep 17 00:00:00 2001 From: Manesh Date: Mon, 6 Oct 2025 03:49:49 +0000 Subject: [PATCH] fix the cost price and few other updates --- app/routes/app._index copy 2.jsx | 412 +++++++++++++ app/routes/app._index copy 3.jsx | 492 ++++++++++++++++ app/routes/app._index copy.jsx | 337 +++++++++++ app/routes/app._index.jsx | 511 ++++++++++------ app/routes/app._index_0110.jsx | 568 ++++++++++++++++++ app/routes/app._index_2909.jsx | 364 ++++++++++++ app/routes/app.billingsuccess.jsx | 377 ++++++++++++ app/routes/app.brands.jsx | 249 ++++---- app/routes/app.brands_060925.jsx | 457 +++++++++++++++ app/routes/app.brands_2808 copy.jsx | 457 +++++++++++++++ app/routes/app.managebrand.jsx | 282 +++++++-- app/routes/app.managebrand_060925.jsx | 805 ++++++++++++++++++++++++++ app/routes/app.settings.jsx | 9 +- app/routes/app.settings_2909.jsx | 347 +++++++++++ package-lock.json | 18 +- package.json | 2 +- 16 files changed, 5365 insertions(+), 322 deletions(-) create mode 100644 app/routes/app._index copy 2.jsx create mode 100644 app/routes/app._index copy 3.jsx create mode 100644 app/routes/app._index copy.jsx create mode 100644 app/routes/app._index_0110.jsx create mode 100644 app/routes/app._index_2909.jsx create mode 100644 app/routes/app.billingsuccess.jsx create mode 100644 app/routes/app.brands_060925.jsx create mode 100644 app/routes/app.brands_2808 copy.jsx create mode 100644 app/routes/app.managebrand_060925.jsx create mode 100644 app/routes/app.settings_2909.jsx diff --git a/app/routes/app._index copy 2.jsx b/app/routes/app._index copy 2.jsx new file mode 100644 index 0000000..abbf5d0 --- /dev/null +++ b/app/routes/app._index copy 2.jsx @@ -0,0 +1,412 @@ +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, +} 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; + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, 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 createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "${PLAN_NAME}" + returnUrl: "https://your-app.com/after-billing" + 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 && loaderData.subscription; + const shop = loaderData && loaderData.shop; + + 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 openModal = () => setActiveModal(true); + const closeModal = () => setActiveModal(false); + + const hasConfirmationUrl = Boolean(actionData && actionData.confirmationUrl); + const errors = (actionData && actionData.errors) || []; + + // Compute trial days left accurately (if TRIAL) + const trialDaysLeft = useMemo(() => { + if (!subscription || !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 && subscription.trialDays, subscription && 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") { + submit(form); + } + }, + } + } + secondaryActions={[{ content: "Close", onAction: closeModal }]} + > +
+ + + + {errors.length > 0 && ( + +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+
+ )} + + {!hasConfirmationUrl && ( + <> + + + + + + + + + + + + + )} + + {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 copy 3.jsx b/app/routes/app._index copy 3.jsx new file mode 100644 index 0000000..16d0115 --- /dev/null +++ b/app/routes/app._index copy 3.jsx @@ -0,0 +1,492 @@ +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; + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, 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 createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "${PLAN_NAME}" + returnUrl: "https://your-app.com/after-billing" + 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 || ""; + + // 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: `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 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 copy.jsx b/app/routes/app._index copy.jsx new file mode 100644 index 0000000..81e6b05 --- /dev/null +++ b/app/routes/app._index copy.jsx @@ -0,0 +1,337 @@ +import React, { useState, useEffect } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, useSubmit, useNavigate } from "@remix-run/react"; +import { + Page, + Layout, + Card, + BlockStack, + Text, + Badge, + InlineStack, + Image, + Divider, + 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"; + + + +// Loader to check subscription status +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + // Query the current subscription status + const resp = await admin.graphql(` + query { + currentAppInstallation { + activeSubscriptions { + id + status + trialDays + createdAt + currentPeriodEnd + } + } + } + `); + + 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,shop }); + } + + // If no active or trial subscription, return redirect signal + if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { + return json({ redirectToBilling: true, subscription ,shop }); + } + + return json({ redirectToBilling: false, subscription,shop }); +}; + +// Action to create subscription +export const action = async ({ request }) => { + console.log("Creating subscription..."); + const { admin } = await authenticate.admin(request); + + const createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "Pro Plan", + returnUrl: "https://your-app.com/after-billing", + lineItems: [ + { + plan: { + appRecurringPricingDetails: { + price: { amount: 19.99, currencyCode: USD }, + interval: EVERY_30_DAYS + } + } + } + ], + trialDays: 7, # βœ… trialDays is a top-level argument! + test: true + ) { + confirmationUrl + appSubscription { + id + status + trialDays + } + userErrors { + field + message + } + } +} + `); + + const data = await createRes.json(); + console.log("Subscription creation response:", data); + if (data.errors || !data.data.appSubscriptionCreate.confirmationUrl) { + return json({ errors: ["Failed to create subscription.",data] }, { status: 400 }); + } + console.log("Subscription created successfully:", data.data.appSubscriptionCreate.confirmationUrl); + return json({ + confirmationUrl: data.data.appSubscriptionCreate.confirmationUrl + }); +}; + +export default function Index() { + const actionData = useActionData(); + const loaderData = useLoaderData(); + const submit = useSubmit(); // Use submit to trigger the action + const [activeModal, setActiveModal] = useState(false); + + const subscription = loaderData?.subscription; + const shop = loaderData?.shop; + console.log("Shop domain from loader data:", subscription); + + // useEffect(() => { + // console.log("Action data:", actionData); + // // If we have a confirmation URL, redirect to it + // if (actionData?.confirmationUrl) { + // window.location.href = actionData.confirmationUrl; // Redirect to Shopify's billing confirmation page + // } + // }, [actionData]); + + +// const navigate = useNavigate(); + +// useEffect(() => { +// if (actionData?.confirmationUrl) { +// navigate(actionData.confirmationUrl, { target: "new" }); // or "host" for embedded +// setActiveModal(false); +// } +// }, [actionData]); + +const navigate = useNavigate(); + +useEffect(() => { + if (actionData?.confirmationUrl) { + navigate(actionData.confirmationUrl, { target: "new" }); // or "new" for a new tab + setActiveModal(false); + } +}, [actionData]); + + // useEffect(() => { + // if (actionData?.confirmationUrl) { + // window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); + // setActiveModal(false); // close the modal + // } + // }, [actionData]); + 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 ( + + + + + + + + + {/* Centered Heading */} + + Welcome to your Turn14 Dashboard + + + {/* Logos Row */} + + 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} + + + + + ))} + + + + + + + + {/* Status Badge */} + + + Status: Connected + Shopify Γ— Turn14 + + + {/* Support Info */} + + Need help? Contact us at{" "} + + support@data4autos.com + + + + {/* CTA Button */} + + + + + + + + + {/* Modal for Subscription Info */} + { + // submit(null, { method: "post", form: document.getElementById("billing-form") }); + // }, + // }} + primaryAction={{ + content: "Proceed to Billing", + onAction: () => { + submit(null, { method: "post", form: document.getElementById("billing-form") }); + }, + }} + secondaryActions={[{ content: "Close", onAction: closeModal }]} + > +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index b8f875c..d94b363 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -1,13 +1,12 @@ -import React, { useState, useEffect } from "react"; +import React, { useMemo, useState } from "react"; import { json } from "@remix-run/node"; -import { useLoaderData, useActionData, useSubmit } from "@remix-run/react"; +import { useLoaderData, useActionData, useSubmit, Form } from "@remix-run/react"; import { Page, Layout, Card, BlockStack, Text, - Badge, InlineStack, Image, Divider, @@ -15,22 +14,28 @@ import { Modal, TextField, Box, - Link, + Banner, + ChoiceList, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; -import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists +import data4autosLogo from "../assets/data4autos_logo.png"; // ensure this exists import turn14DistributorLogo from "../assets/turn14-logo.png"; -import { authenticate } from "../shopify.server"; // Shopify server authentication +import { authenticate } from "../shopify.server"; -import { Form } from "@remix-run/react"; +/* =========================== + 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 to check subscription status +/* =========================== + LOADER: check subscription + =========================== */ export const loader = async ({ request }) => { const { admin } = await authenticate.admin(request); - // Query the current subscription status const resp = await admin.graphql(` query { currentAppInstallation { @@ -46,119 +51,196 @@ export const loader = async ({ request }) => { `); const result = await resp.json(); - const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null; + 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; + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, 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 { 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,shop }); - } + const shopDomain = (shop || "").split(".")[0]; - // If no active or trial subscription, return redirect signal - if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { - return json({ redirectToBilling: true, subscription ,shop }); - } - return json({ redirectToBilling: false, subscription,shop }); -}; -// Action to create subscription -export const action = async ({ request }) => { - console.log("Creating subscription..."); - const { admin } = await authenticate.admin(request); + 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: "Pro Plan", - returnUrl: "https://your-app.com/after-billing", - lineItems: [ - { - plan: { - appRecurringPricingDetails: { - price: { amount: 19.99, currencyCode: USD }, - interval: EVERY_30_DAYS + 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 } } - ], - trialDays: 7, # βœ… trialDays is a top-level argument! - test: true - ) { - confirmationUrl - appSubscription { - id - status - trialDays } - userErrors { - field - message - } - } -} `); const data = await createRes.json(); - console.log("Subscription creation response:", data); - if (data.errors || !data.data.appSubscriptionCreate.confirmationUrl) { - return json({ errors: ["Failed to create subscription."] }, { status: 400 }); + + 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 } + ); } - console.log("Subscription created successfully:", data.data.appSubscriptionCreate.confirmationUrl); - return json({ - confirmationUrl: data.data.appSubscriptionCreate.confirmationUrl - }); + + return json({ confirmationUrl: url }); }; +/* =========================== + PAGE + =========================== */ export default function Index() { const actionData = useActionData(); const loaderData = useLoaderData(); - const submit = useSubmit(); // Use submit to trigger the action + const submit = useSubmit(); const [activeModal, setActiveModal] = useState(false); - const subscription = loaderData?.subscription; - const shop = loaderData?.shop; + const subscription = loaderData?.subscription || null; + const shop = loaderData?.shop || ""; - // useEffect(() => { - // console.log("Action data:", actionData); - // // If we have a confirmation URL, redirect to it - // if (actionData?.confirmationUrl) { - // window.location.href = actionData.confirmationUrl; // Redirect to Shopify's billing confirmation page - // } - // }, [actionData]); + // 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`, + }, + ]; - useEffect(() => { - if (actionData?.confirmationUrl) { - window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); - setActiveModal(false); // close the modal - } - }, [actionData]); 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" }, - // ]; + /* =========================== + Helpers & preview model + =========================== */ + const formatDate = (d) => + new Date(d).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); - const shopDomain = (shop || "").split(".")[0];; // from the GraphQL query above + // 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; -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` }, -]; + // 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 ( @@ -167,20 +249,13 @@ const items = [ - - {/* Centered Heading */} Welcome to your Turn14 Dashboard - {/* Logos Row */} - Data4Autos Logo + 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. + product brands, manage collections, and automate catalog setup + directly from Turn14 to your Shopify store. + - {/* πŸ”§ */} Use the left sidebar to: - ( - + {item.icon} - + {item.text} @@ -233,82 +319,185 @@ const items = [ - {/* Status Badge */} - - - Status: Connected - Shopify Γ— Turn14 - - - {/* Support Info */} Need help? Contact us at{" "} - - support@data4autos.com - + support@data4autos.com - {/* CTA Button */} - - - {/* Modal for Subscription Info */} + {/* =========================== + MODAL + =========================== */} { - // submit(null, { method: "post", form: document.getElementById("billing-form") }); - // }, - // }} - primaryAction={{ - content: "Proceed to Billing", - onAction: () => { - submit(null, { method: "post", form: document.getElementById("billing-form") }); - }, - }} + primaryAction={ + hasConfirmationUrl + ? 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 && 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_0110.jsx b/app/routes/app._index_0110.jsx new file mode 100644 index 0000000..47a5135 --- /dev/null +++ b/app/routes/app._index_0110.jsx @@ -0,0 +1,568 @@ +import React, { useMemo, useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, useSubmit } from "@remix-run/react"; +import { + Page, + Layout, + Card, + BlockStack, + Text, + InlineStack, + Image, + Divider, + Button, + Modal, + Box, + Banner, + Badge, + Tooltip, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import data4autosLogo from "../assets/data4autos_logo.png"; +import turn14DistributorLogo from "../assets/turn14-logo.png"; +import { authenticate } from "../shopify.server"; +import { Form } from "@remix-run/react"; + +/* =========================== + PLAN CATALOG (edit freely) + =========================== */ +const PLANS = [ + { + id: "starter", + name: "Starter Sync", + badge: "New", + highlight: false, + features: [ + "Guided Turn14 β†’ Shopify import", + "Auto inventory & price updates", + "Editable title/description helpers", + "Email support", + ], + periods: [ + { id: "monthly", label: "Monthly", interval: "EVERY_30_DAYS", amount: 79, currencyCode: "USD" }, + { id: "annual", label: "Annual", interval: "ANNUAL", amount: 790, currencyCode: "USD", sublabel: "2 months free" }, + ], + }, + { + id: "growth", + name: "Growth", + badge: "Popular", + highlight: true, + features: [ + "Everything in Starter", + "Bulk brand imports", + "Smart collection sync", + "Basic error diagnostics", + ], + periods: [ + { id: "monthly", label: "Monthly", interval: "EVERY_30_DAYS", amount: 129, currencyCode: "USD" }, + { id: "annual", label: "Annual", interval: "ANNUAL", amount: 1290, currencyCode: "USD", sublabel: "2 months free" }, + ], + }, + { + id: "pro", + name: "Pro", + badge: "For Teams", + highlight: false, + features: [ + "Everything in Growth", + "Automated brand metadata", + "Advanced mapping rules", + "Priority email support", + ], + periods: [ + { id: "monthly", label: "Monthly", interval: "EVERY_30_DAYS", amount: 199, currencyCode: "USD" }, + { id: "annual", label: "Annual", interval: "ANNUAL", amount: 1990, currencyCode: "USD", sublabel: "2 months free" }, + ], + }, + { + id: "scale", + name: "Scale", + badge: "Best Value", + highlight: false, + features: [ + "Everything in Pro", + "Unlimited brand sync", + "Enhanced audit logs", + "Slack alerts", + ], + periods: [ + { id: "monthly", label: "Monthly", interval: "EVERY_30_DAYS", amount: 399, currencyCode: "USD" }, + { id: "annual", label: "Annual", interval: "ANNUAL", amount: 3990, currencyCode: "USD", sublabel: "2 months free" }, + ], + }, + { + id: "enterprise", + name: "Enterprise", + badge: "Custom", + highlight: false, + features: [ + "Everything in Scale", + "SLA & dedicated onboarding", + "Solution architect sessions", + "Custom integrations", + ], + periods: [ + { id: "monthly", label: "Monthly", interval: "EVERY_30_DAYS", amount: 799, currencyCode: "USD" }, + { id: "annual", label: "Annual", interval: "ANNUAL", amount: 7990, currencyCode: "USD", sublabel: "2 months free" }, + ], + }, +]; + +/* =========================== + 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?.data?.currentAppInstallation?.activeSubscriptions?.[0] || null; + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, shop, plans: PLANS }); + } + + if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { + return json({ redirectToBilling: true, subscription, shop, plans: PLANS }); + } + + return json({ redirectToBilling: false, subscription, shop, plans: PLANS }); +}; + +/* =========================== + ACTION: create subscription + =========================== */ +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const formData = await request.formData(); + + const planId = String(formData.get("planId") || ""); + const periodId = String(formData.get("periodId") || ""); + const trialDays = Number(formData.get("trialDays") || 14); // change default if needed + + const plan = PLANS.find((p) => p.id === planId); + const period = plan?.periods.find((pr) => pr.id === periodId); + + if (!plan || !period) { + return json( + { errors: ["Invalid plan or billing period selection. Please try again."] }, + { status: 400 } + ); + } + + // Build a safe returnUrl from the incoming request origin + const urlObj = new URL(request.url); + const origin = `${urlObj.protocol}//${urlObj.host}`; + const returnUrl = `${origin}/app/after-billing`; // adjust if your route differs + + const gql = ` + mutation appSubscriptionCreate( + $name: String!, + $returnUrl: URL!, + $price: MoneyInput!, + $interval: AppBillingInterval!, + $trialDays: Int, + $test: Boolean + ) { + appSubscriptionCreate( + name: $name + returnUrl: $returnUrl + lineItems: [ + { + plan: { + appRecurringPricingDetails: { + price: $price + interval: $interval + } + } + } + ] + trialDays: $trialDays + test: $test + ) { + confirmationUrl + appSubscription { id status trialDays } + userErrors { field message } + } + } + `; + + const variables = { + name: `${plan.name} (${period.label})`, + returnUrl, + price: { amount: period.amount, currencyCode: period.currencyCode }, + interval: period.interval, // "EVERY_30_DAYS" | "ANNUAL" + trialDays, + test: true, // flip to false in production + }; + + const createRes = await admin.graphql(gql, { variables }); + const data = await createRes.json(); + + const confirmationUrl = data?.data?.appSubscriptionCreate?.confirmationUrl; + const userErrors = data?.data?.appSubscriptionCreate?.userErrors || []; + const topLevelErrors = data?.errors || []; + + if (!confirmationUrl || 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 }); +}; + +/* =========================== + PAGE + =========================== */ +export default function Index() { + const actionData = useActionData(); + const loaderData = useLoaderData(); + const submit = useSubmit(); + const [activeModal, setActiveModal] = useState(false); + + const subscription = loaderData?.subscription; + const shop = loaderData?.shop; + const plans = loaderData?.plans || PLANS; + + 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 openModal = () => setActiveModal(true); + const closeModal = () => setActiveModal(false); + + const hasConfirmationUrl = Boolean(actionData?.confirmationUrl); + const errors = actionData?.errors || []; + + // Selection state (for the beautiful plan picker) + const [selectedPlanId, setSelectedPlanId] = useState(plans[0]?.id ?? "starter"); + const selectedPlan = useMemo( + () => plans.find((p) => p.id === selectedPlanId), + [plans, selectedPlanId] + ); + + // default to monthly + const [selectedPeriodId, setSelectedPeriodId] = useState( + selectedPlan?.periods?.[0]?.id ?? "monthly" + ); + + // keep period valid when switching plans + React.useEffect(() => { + if (!selectedPlan) return; + const exists = selectedPlan.periods.some((p) => p.id === selectedPeriodId); + if (!exists) setSelectedPeriodId(selectedPlan.periods[0].id); + }, [selectedPlanId]); + + return ( + + + + + + + + + Welcome to your Turn14 Dashboard + + + + Data4Autos Logo + Turn14 Distributors Logo + + + + + + + + πŸš€ Data4Autos Turn14 Integration gives you the power to sync brands, + manage collections, and automate catalog setup directly from Turn14 to your Shopify store. + + + + + Quick links + + + + {items.map((item, index) => ( + + + + {item.icon} + + + + {item.text} + + + + + ))} + + + + + + + + + Need help? Contact us at{" "} + support@data4autos.com + + + + {subscription ? ( + + {subscription.status} β€’ Trial: {subscription.trialDays ?? 0}d + + ) : ( + No active subscription + )} + + + + + + + + + + {/* =========================== + BEAUTIFUL PLAN PICKER MODAL + =========================== */} + { + const form = document.getElementById("billing-form") || null; + if (form) submit(form); + }, + disabled: !selectedPlan || !selectedPeriodId, + } + } + secondaryActions={[{ content: "Close", onAction: closeModal }]} + large + > +
+ + + {errors.length > 0 && ( + +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+
+ )} + + {!hasConfirmationUrl && ( + <> + {/* Hidden fields sent to the action */} + + + + + {/* Period toggle (Monthly / Annual) */} + + {selectedPlan?.periods.map((period) => { + const isActive = selectedPeriodId === period.id; + return ( + + ); + })} + + + {/* Plan tiles */} + + {plans.map((plan) => { + const active = selectedPlanId === plan.id; + const p = plan.periods.find((pp) => pp.id === selectedPeriodId) ?? plan.periods[0]; + + return ( + setSelectedPlanId(plan.id)} + padding="500" + background={active ? "bg-surface" : "bg-surface-secondary"} + sectioned + roundedAbove="sm" + style={{ + gridColumn: "span 6", + cursor: "pointer", + border: active ? "2px solid var(--p-color-border-interactive)" : "1px solid var(--p-color-border-subdued)", + boxShadow: active ? "var(--p-shadow-lg)" : "var(--p-shadow-sm)", + transition: "all .2s ease", + }} + > + + + + {plan.name} + + {plan.badge && ( + {plan.badge} + )} + + + + + ${p.amount} + + + {p.interval === "ANNUAL" ? "/yr" : "/mo"} β€’ {p.currencyCode} + + + + + + + {plan.features.map((f) => ( + + βœ… + {f} + + ))} + + + + + + 14-day free trial + + + + + ); + })} + + + )} + + {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_2909.jsx b/app/routes/app._index_2909.jsx new file mode 100644 index 0000000..68177c3 --- /dev/null +++ b/app/routes/app._index_2909.jsx @@ -0,0 +1,364 @@ +import React, { useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, useSubmit } from "@remix-run/react"; +import { + Page, + Layout, + Card, + BlockStack, + Text, + Badge, + InlineStack, + Image, + Divider, + Button, + Modal, + TextField, + Box, + Banner, +} 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"; +import { Form } from "@remix-run/react"; + +/* =========================== + 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?.data?.currentAppInstallation?.activeSubscriptions?.[0] || null; + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + if (!subscription) { + return json({ redirectToBilling: true, subscription: null, shop }); + } + + if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { + return json({ redirectToBilling: true, subscription, shop }); + } + + return json({ redirectToBilling: false, subscription, shop }); +}; + +/* =========================== + ACTION: create subscription + =========================== */ +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + const createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "Pro Plan" + returnUrl: "https://your-app.com/after-billing" + lineItems: [ + { + plan: { + appRecurringPricingDetails: { + price: { amount: 79, currencyCode: USD } + interval: EVERY_30_DAYS + } + } + } + ] + trialDays: 7 + test: true + ) { + confirmationUrl + appSubscription { id status trialDays } + userErrors { field message } + } + } + `); + + const data = await createRes.json(); + + const url = data?.data?.appSubscriptionCreate?.confirmationUrl; + const userErrors = 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; + const shop = loaderData?.shop; + + 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 openModal = () => setActiveModal(true); + const closeModal = () => setActiveModal(false); + + const hasConfirmationUrl = Boolean(actionData?.confirmationUrl); + const errors = actionData?.errors || []; + + 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} + + {/* Real anchor to avoid double-URL issues */} + + + {item.text} + + + + + ))} + + + + + + + + {/* + Status: Connected + Shopify Γ— Turn14 + */} + + + Need help? Contact us at{" "} + support@data4autos.com + + + + + + + + + + {/* =========================== + MODAL + =========================== */} + { + const form = document.getElementById("billing-form"); + submit(form); + }, + } + } + secondaryActions={[{ content: "Close", onAction: closeModal }]} + > +
+ + + {errors.length > 0 && ( + +
    + {errors.map((e, i) => ( +
  • {e}
  • + ))} +
+
+ )} + + {!hasConfirmationUrl && ( + <> + + + + + + )} + + {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. + + + {/* Polaris Button-as-link opens absolute URL in a new tab */} + + + {/* Plain anchor fallback (optional) */} + + {actionData.confirmationUrl} + + + )} +
+
+
+
+
+ ); +} diff --git a/app/routes/app.billingsuccess.jsx b/app/routes/app.billingsuccess.jsx new file mode 100644 index 0000000..1d59c9d --- /dev/null +++ b/app/routes/app.billingsuccess.jsx @@ -0,0 +1,377 @@ +// app/routes/app.billing.success.jsx +import React, { useMemo } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { + Page, + Layout, + Card, + BlockStack, + InlineStack, + Text, + Badge, + Divider, + Button, + Banner, + Box, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +/** =========================== + * LOADER + * =========================== */ +export const loader = async ({ request }) => { + const { admin, session } = await authenticate.admin(request); + const shop = session.shop; + const shopDomain = shop.split(".")[0]; + + // Pull richer subscription details: interval + price + const resp = await admin.graphql(` + query ActiveSubForSuccess { + currentAppInstallation { + activeSubscriptions { + id + name + status + trialDays + createdAt + currentPeriodEnd + test + lineItems { + plan { + appRecurringPricingDetails { + interval + price { amount currencyCode } + } + } + } + } + } + } + `); + + const result = await resp.json(); + const subscription = + result?.data?.currentAppInstallation?.activeSubscriptions?.[0] || null; + + // Detect recent activation (today or last 2 days) + let recentActivation = false; + if (subscription?.createdAt) { + const created = new Date(subscription.createdAt); + const now = new Date(); + const diffMs = now.getTime() - created.getTime(); + const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000; + recentActivation = + diffMs >= 0 && + diffMs <= TWO_DAYS_MS && + (subscription.status === "ACTIVE" || subscription.status === "TRIAL"); + } + + return json({ subscription, shop, shopDomain, recentActivation }); +}; + +/** =========================== + * HELPERS + * =========================== */ +function formatDate(d) { + if (!d) return "N/A"; + return new Date(d).toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function getRecurringLine(subscription) { + const items = subscription?.lineItems || []; + for (const li of items) { + const r = li?.plan?.appRecurringPricingDetails; + if (r) return r; + } + return null; +} + +function intervalToLabel(interval) { + switch (interval) { + case "ANNUAL": + return "Annual"; + case "EVERY_30_DAYS": + return "Monthly"; + default: + return interval || "N/A"; + } +} + +/** =========================== + * PAGE + * =========================== */ +export default function BillingSuccess() { + const { subscription, shop, shopDomain, recentActivation } = useLoaderData(); + + const recurring = getRecurringLine(subscription); + const cadenceLabel = intervalToLabel(recurring?.interval); + const priceText = + recurring?.price?.amount && recurring?.price?.currencyCode + ? `${recurring.price.amount} ${recurring.price.currencyCode}` + : "N/A"; + + // Trial end + const trialEndStr = useMemo(() => { + if (!subscription?.trialDays || !subscription?.createdAt) return "N/A"; + const start = new Date(subscription.createdAt); + const end = new Date(start); + end.setDate(end.getDate() + subscription.trialDays); + return formatDate(end); + }, [subscription?.trialDays, subscription?.createdAt]); + + // Trial days left (if still on TRIAL) + const trialDaysLeft = useMemo(() => { + if ( + subscription?.status !== "TRIAL" || + !subscription?.trialDays || + !subscription?.createdAt + ) + return null; + const start = new Date(subscription.createdAt); + const trialEnd = new Date(start); + trialEnd.setDate(trialEnd.getDate() + subscription.trialDays); + const now = new Date(); + const left = Math.ceil( + (trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) + ); + return Math.max(0, left); + }, [subscription?.status, subscription?.trialDays, subscription?.createdAt]); + + const showError = !subscription; + const nextRenewal = formatDate(subscription?.currentPeriodEnd); + const createdAt = formatDate(subscription?.createdAt); + + return ( + + + + + {showError ? ( + +

+ We couldn’t find an active subscription for this shop. If you just + approved billing, it may not be visible yet. You can proceed to the + billing screen to create or refresh your subscription. +

+ + + + +
+ ) : recentActivation ? ( + +

+ Congratulations! Your plan is now active. You’re all set to sync brands, + build collections, and automate your Turn14 catalog. +

+

+ Activated: {createdAt}  β€’  Status:{" "} + {subscription.status} +

+ + + + +
+ ) : ( + +

+ Your subscription is active. Below are the full details of your plan, + trial, and renewal. +

+
+ )} +
+ + + + + + Plan Overview + + + + + + + Plan Name + + + {subscription?.name || "Starter Sync"} + + + + + + + + Billing Cadence + + + {cadenceLabel} + + + + + + + + Price + + + {priceText} + + + + + + + + + Billing & Trial + + + + + Status + + + {subscription?.status || "N/A"} + + {subscription?.test && Test} + + + + + + + Trial + + {subscription?.trialDays ? `${subscription.trialDays} days` : "N/A"} + {trialDaysLeft != null && β€” {trialDaysLeft} day(s) left} + + + + + + + Trial Ends + {trialEndStr} + + + + + + Next Renewal / Period End + {nextRenewal} + + + + + + + + Subscription Metadata + + + + + Subscription ID + + {subscription?.id || "N/A"} + + + + + + + Created / Activated + {createdAt} + + + + + + Shop + {shop} + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx index 203105e..2601313 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -38,68 +38,81 @@ async function checkShopExists(shop) { } 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); - // } - - - - +// const accessToken = await getTurn14AccessTokenFromMetafield(request); + const { admin } = await authenticate.admin(request); const { session } = await authenticate.admin(request); const shop = session.shop; - return json({ brands: [], collections : [], selectedBrandsFromShopify: [], 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 { + 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 || [], shop }); }; export const action = async ({ request }) => { @@ -137,9 +150,15 @@ export const action = async ({ request }) => { }; export default function BrandsPage() { - const { brands, collections, selectedBrandsFromShopify, shop } = useLoaderData(); + const { brands, collections, selectedBrandsFromShopify, shop ,err} = useLoaderData(); + console.log(err) // 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())); @@ -265,65 +284,65 @@ export default function BrandsPage() { ]; // 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. + if (Turn14Enabled === false) { + return ( + + + + + + +
+ + Turn14 isn’t connected yet -
- - {/* Primary actions */} - - -
- - Once connected, you’ll be able to browse brands and sync collections. - -
- - {/* Secondary links */} - -
- - - - - - ); -} + + + + + + ); + } @@ -338,7 +357,7 @@ if (Turn14Enabled === false) { Data4Autos Turn14 Brands List
- +
{/*

diff --git a/app/routes/app.brands_060925.jsx b/app/routes/app.brands_060925.jsx new file mode 100644 index 0000000..c3b589c --- /dev/null +++ b/app/routes/app.brands_060925.jsx @@ -0,0 +1,457 @@ +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"; + + + + + + + +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); + + // // 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); + // } + + + + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + return json({ brands: [], collections : [], selectedBrandsFromShopify: [], shop }); +}; + +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, 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(() => { + 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 || ""); + + + + + + + + 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) + 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)); + + + + 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..."} +

+
*/} + + + + + +
+ + {/* 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.brands_2808 copy.jsx b/app/routes/app.brands_2808 copy.jsx new file mode 100644 index 0000000..e95979e --- /dev/null +++ b/app/routes/app.brands_2808 copy.jsx @@ -0,0 +1,457 @@ +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"; + + + + + + + +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); + + // 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); + } + + + + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + return json({ brands: brandJson.data, collections, selectedBrandsFromShopify: brands || [], shop }); +}; + +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, 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(() => { + 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 || ""); + + + + + + + + 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) + 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)); + + + + 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..."} +

+
*/} + + + + + +
+ + {/* 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.managebrand.jsx b/app/routes/app.managebrand.jsx index 4622fd0..8d25160 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -19,10 +19,30 @@ import { 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 { @@ -40,9 +60,29 @@ async function checkShopExists(shop) { export const loader = async ({ request }) => { - const { admin } = await authenticate.admin(request); + + const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); - const accessToken = await getTurn14AccessTokenFromMetafield(request); + + + + 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 { @@ -62,8 +102,7 @@ export const loader = async ({ request }) => { } - const { session } = await authenticate.admin(request); - const shop = session.shop; + return json({ brands, accessToken, shop }); }; @@ -280,7 +319,6 @@ export default function ManageBrandProducts() { const [results, setResults] = useState([]); const [detail, setDetail] = useState(""); - const [filterregulatstock, setfilterregulatstock] = useState(false) @@ -409,6 +447,13 @@ export default function ManageBrandProducts() { 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 })); @@ -445,6 +490,26 @@ export default function ManageBrandProducts() { 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; }); }; @@ -456,17 +521,38 @@ export default function ManageBrandProducts() { - 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 + 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 ( @@ -483,7 +569,7 @@ export default function ManageBrandProducts() { 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 */} - +
@@ -525,14 +611,14 @@ export default function ManageBrandProducts() { ); } - + return ( - {/*

+ {/*

Turn 14 Status:{" "} {Turn14Enabled === true ? "βœ… Turn14 x Shopify Connected!" @@ -611,7 +697,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(), @@ -708,11 +794,47 @@ export default function ManageBrandProducts() { 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)} + /> + + + + + + + {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" + /> + + { setfilterregulatstock(!filterregulatstock) }} + /> + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + {actionData?.error && !actionData?.pricingSaved && ( + + + + )} + + {(actionData?.success || Boolean(savedCreds.accessToken)) && ( + +

βœ… Turn14 connected successfully!

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