MOHAN d791414f27 fix: bypass subscription gate for free-access shops on brands/manage-brand pages
Both app.brands.jsx and app.managebrand.jsx now call /free-access/:shop
after a negative subscription check, mirroring the fix already applied
in app._index.jsx. Affects both loader (UI lock) and action (import guard).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:46:13 +05:30

404 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react";
import {
Page,
Layout,
Card,
TextField,
Checkbox,
Button,
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) {
if (interval === "ANNUAL") return "Every 12 months";
if (interval === "EVERY_30_DAYS") return "Every 30 days";
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;
} catch (err) {
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop: "", error: "Shopify authentication failed", isSubscribed: false, subscription: null });
}
let { isSubscribed, subscription } = await getSubscriptionDetails(request);
if (!isSubscribed) {
try {
const far = await fetch(`https://backend.data4autos.com/free-access/${encodeURIComponent(shop)}`);
const fad = await far.json();
if (fad.allowed === true) isSubscribed = true;
} catch {}
}
let accessToken = "";
try {
accessToken = await getTurn14AccessTokenFromMetafield(request);
} catch (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) {
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) {}
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) {}
return json({ brands: brandJson?.data || [], collections, selectedBrandsFromShopify: selectedBrands, shop, isSubscribed, subscription });
};
export const action = async ({ request }) => {
const { session } = await authenticate.admin(request);
const shop = session.shop;
let { isSubscribed } = await getSubscriptionDetails(request);
if (!isSubscribed) {
try {
const far = await fetch(`https://backend.data4autos.com/free-access/${encodeURIComponent(shop)}`);
const fad = await far.json();
if (fad.allowed === true) isSubscribed = true;
} catch {}
}
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") || "[]");
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);
const [saving, setSaving] = useState(false);
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); setSaving(false); }
if (actionData.error) setSaving(false);
}, [actionData.status, actionData.error]);
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])));
};
const trialDaysLeft = useMemo(() => {
if (!subscription?.trialDays || !subscription?.createdAt || subscription.status !== "TRIAL") return null;
const created = new Date(subscription.createdAt);
const trialEnd = new Date(created);
trialEnd.setDate(trialEnd.getDate() + subscription.trialDays);
return Math.max(0, Math.ceil((trialEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24)));
}, [subscription]);
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
const selectedOldBrands = brands.filter((b) => selectedIdsold.includes(b.id));
const shopDomain = (shop || "").split(".")[0];
if (Turn14Enabled === false) {
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff" }}>
<div style={{ fontSize: 22, fontWeight: 800 }}>🏷 Brand Selection</div>
<div style={{ fontSize: 14, opacity: 0.8, marginTop: 4 }}>{shop}</div>
</div>
<Card>
<div style={{ padding: 48, textAlign: "center" }}>
<div style={{ fontSize: 48, marginBottom: 12 }}>🔌</div>
<Text as="h1" variant="headingLg">Turn14 not connected yet</Text>
<div style={{ marginTop: 8, color: "#6b7280", marginBottom: 24 }}>Complete the connection in Settings before browsing brands.</div>
<div style={{ display: "flex", gap: 12, justifyContent: "center" }}>
<Button variant="primary" onClick={() => navigate("/app/settings")}>Go to Settings</Button>
<Button onClick={() => navigate("/app/help")}>View Help</Button>
</div>
</div>
</Card>
</Page>
</Frame>
);
}
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
{/* Header */}
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
<div>
<div style={{ fontSize: 22, fontWeight: 800 }}>🏷 Brand Selection</div>
<div style={{ fontSize: 14, opacity: 0.8, marginTop: 4 }}>{filteredBrands.length} brands available · {selectedIds.length} selected</div>
</div>
{!isSubscribed && (
<div style={{ background: "rgba(251,191,36,0.2)", border: "1px solid rgba(251,191,36,0.4)", borderRadius: 8, padding: "8px 14px", fontSize: 13, color: "#fbbf24", fontWeight: 700 }}>
Subscription required
</div>
)}
</div>
{/* Subscription warning */}
{!isSubscribed && (
<div style={{ background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 12, padding: "16px 20px", marginBottom: 20 }}>
<div style={{ fontWeight: 700, color: "#b45309", marginBottom: 6 }}> Active subscription required</div>
<div style={{ fontSize: 13, color: "#92400e", marginBottom: 12 }}>Brand selection is only available with an active subscription or free trial.</div>
<Button variant="primary" onClick={() => navigate("/app")}>Manage Subscription</Button>
</div>
)}
{error && (
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 12, padding: "14px 18px", marginBottom: 20, color: "#dc2626" }}>
{error}
</div>
)}
{actionData?.error && (
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 12, padding: "14px 18px", marginBottom: 20, color: "#dc2626" }}>
{actionData.error}
</div>
)}
{/* Sticky toolbar */}
<div style={{ position: "sticky", top: 0, zIndex: 10, background: "#fff", borderRadius: 12, boxShadow: "0 2px 12px rgba(0,0,0,0.08)", padding: "14px 20px", marginBottom: 20, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
<div style={{ display: "flex", gap: 14, alignItems: "center", flexWrap: "wrap" }}>
<div style={{ minWidth: 280 }}>
<TextField
labelHidden
label="Search brands"
value={search}
onChange={setSearch}
placeholder="🔍 Search brands…"
autoComplete="off"
disabled={!isSubscribed}
/>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Checkbox label="" checked={allFilteredSelected} onChange={toggleSelectAll} disabled={!isSubscribed} />
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151", cursor: "pointer" }} onClick={toggleSelectAll}>Select All</span>
</div>
<div style={{ fontSize: 13, color: "#6b7280" }}>
<span style={{ fontWeight: 700, color: "#1d4ed8" }}>{selectedIds.length}</span> selected
</div>
{(actionData?.processId) && (
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div style={{ fontSize: 13, color: "#374151" }}><strong>Status:</strong> {status || ""}</div>
<Button onClick={checkStatus} loading={polling} size="slim">Refresh</Button>
</div>
)}
</div>
<Form method="post" onSubmit={() => setSaving(true)}>
<input type="hidden" name="selectedBrands" value={JSON.stringify(selectedBrands)} />
<input type="hidden" name="selectedOldBrands" value={JSON.stringify(selectedOldBrands)} />
<button
type="submit"
disabled={saving || !isSubscribed}
style={{ background: saving || !isSubscribed ? "#9ca3af" : "linear-gradient(135deg, #1d4ed8, #2563eb)", border: "none", borderRadius: 8, color: "#fff", padding: "10px 22px", cursor: saving || !isSubscribed ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 14, display: "flex", alignItems: "center", gap: 8 }}
>
{saving && <Spinner size="small" />}
{saving ? "Saving…" : `Save ${selectedIds.length} Collections`}
</button>
</Form>
</div>
{/* Brand grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 14 }}>
{filteredBrands.map((brand) => {
const isSelected = selectedIds.includes(brand.id);
return (
<div
key={brand.id}
onClick={() => toggleSelect(brand.id)}
style={{ background: isSelected ? "#eff6ff" : "#fff", border: `2px solid ${isSelected ? "#2563eb" : "#e5e7eb"}`, borderRadius: 12, padding: "16px 14px", cursor: isSubscribed ? "pointer" : "default", position: "relative", transition: "all 0.15s", boxShadow: isSelected ? "0 0 0 3px rgba(37,99,235,0.12)" : "none" }}
>
{/* Checkbox badge */}
<div style={{ position: "absolute", top: 10, right: 10 }}>
<div style={{ width: 20, height: 20, borderRadius: "50%", background: isSelected ? "#2563eb" : "#e5e7eb", border: `2px solid ${isSelected ? "#2563eb" : "#d1d5db"}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
{isSelected && <span style={{ color: "#fff", fontSize: 11 }}></span>}
</div>
</div>
<div style={{ display: "flex", justifyContent: "center", marginBottom: 12 }}>
<img
src={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
alt={brand.name}
style={{ width: 72, height: 72, objectFit: "contain", borderRadius: 8 }}
/>
</div>
<div style={{ textAlign: "center", fontWeight: 700, fontSize: 14, color: isSelected ? "#1d4ed8" : "#374151", lineHeight: 1.3 }}>
{brand.name}
</div>
<div style={{ textAlign: "center", fontSize: 11, color: "#9ca3af", marginTop: 4 }}>ID: {brand.id}</div>
</div>
);
})}
</div>
{filteredBrands.length === 0 && (
<div style={{ textAlign: "center", padding: 48, color: "#9ca3af" }}>
<div style={{ fontSize: 36, marginBottom: 12 }}>🔍</div>
<div style={{ fontSize: 16, fontWeight: 600 }}>No brands found</div>
<div style={{ fontSize: 13, marginTop: 6 }}>Try a different search term</div>
</div>
)}
{toastActive && (
<Toast content="Collections saved successfully!" onDismiss={() => setToastActive(false)} />
)}
</Page>
</Frame>
);
}