import { json } from "@remix-run/node"; import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react"; import { Page, Layout, Card, TextField, Checkbox, Button, Thumbnail, Spinner, Toast, Frame, Text, Banner, InlineStack, } from "@shopify/polaris"; 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 { const resp = await fetch( `https://backend.data4autos.com/checkisshopdataexists/${shop}` ); const data = await resp.json(); return data.status === 1; } catch (err) { console.error("Error checking shop:", err); return false; } } 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; const resp = await admin.graphql(` query CurrentSubscriptionDetails { currentAppInstallation { activeSubscriptions { id name status 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 = 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, }; } export const loader = async ({ request }) => { console.log("πŸš€ Loader started"); let admin, session, shop; try { 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("❌ 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(); 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; 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 navigate = useNavigate(); const { brands = [], selectedBrandsFromShopify = [], shop = "", error, isSubscribed = false, subscription = null, } = useLoaderData() || {}; const actionData = useActionData() || {}; 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); useEffect(() => { if (!shop) return; (async () => { const result = await checkShopExists(shop); setTurn14Enabled(result); })(); }, [shop]); useEffect(() => { setSelectedIdsold(selectedIds); }, [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) => { 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)); const toggleSelectAll = () => { if (!isSubscribed) return; 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]))); } }; let isSubmitting = false; if (actionData.status) { isSubmitting = !actionData.status && !actionData.error && !actionData.processId; } 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`, }, ]; 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 (
Turn14 isn’t connected yet
This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
Once connected, you’ll be able to browse brands and sync collections.
); } return (
Data4Autos Turn14 Brands List
{!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 || "β€”"}

)}
{filteredBrands.map((brand) => (
toggleSelect(brand.id)} disabled={!isSubscribed} />
{brand.name}
))}
{toastMarkup}
); }