import React, { useMemo, useState } from "react"; import { json } from "@remix-run/node"; import { useLoaderData, useActionData, useSubmit, Form, useNavigate } from "@remix-run/react"; import { Page, Modal, TextField, BlockStack, InlineStack, ChoiceList, Banner, Button, Divider, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import { authenticate } from "../shopify.server"; /* ─── pricing constants ────────────────────────────────────────────────────── */ const PLAN_NAME = "Starter Sync"; const MONTHLY_AMOUNT = 79; const ANNUAL_AMOUNT = 790; const TRIAL_DAYS = 14; const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"]; /* ─── helpers ──────────────────────────────────────────────────────────────── */ function formatMoney(amount, currencyCode = "USD") { if (amount == null) return "N/A"; return `${currencyCode} ${Number(amount).toFixed(2)}`; } function getIntervalLabel(interval) { if (interval === "ANNUAL") return "Every 12 months"; if (interval === "EVERY_30_DAYS") return "Every 30 days"; return interval || "N/A"; } function getStatusTone(status) { if (status === "ACTIVE") return { bg: "#f0fdf4", border: "#bbf7d0", color: "#15803d" }; if (status === "TRIAL") return { bg: "#eff6ff", border: "#bfdbfe", color: "#1d4ed8" }; if (["CANCELLED", "EXPIRED", "DECLINED"].includes(status)) return { bg: "#fff1f2", border: "#fecdd3", color: "#dc2626" }; return { bg: "#fefce8", border: "#fde68a", color: "#d97706" }; } /* ─── loader ────────────────────────────────────────────────────────────────── */ export const loader = async ({ 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); const subscriptionDetails = subscription ? { id: subscription.id, name: subscription.name || PLAN_NAME, status: subscription.status, test: subscription.test ?? false, createdAt: subscription.createdAt, trialDays: subscription.trialDays ?? 0, currentPeriodEnd: subscription.currentPeriodEnd, interval: recurringPricing?.interval || null, priceAmount: recurringPricing?.price?.amount || null, currencyCode: recurringPricing?.price?.currencyCode || "USD", } : null; // Check server-side free-access whitelist let hasFreeAccess = false; try { const far = await fetch(`https://backend.data4autos.com/free-access/${encodeURIComponent(shop)}`); const fad = await far.json(); hasFreeAccess = fad.allowed === true; } catch {} if (hasFreeAccess) { return json({ redirectToBilling: false, shop, isSubscribed: true, subscription: subscriptionDetails, allSubscriptions: subscriptions }); } return json({ redirectToBilling: !isSubscribed, shop, isSubscribed, subscription: subscriptionDetails, allSubscriptions: subscriptions }); }; /* ─── action ────────────────────────────────────────────────────────────────── */ export const action = async ({ request }) => { const { admin, session } = await authenticate.admin(request); const form = await request.formData(); const rawCadence = form.get("cadence"); const cadence = rawCadence === "ANNUAL" ? "ANNUAL" : "MONTHLY"; const interval = cadence === "ANNUAL" ? "ANNUAL" : "EVERY_30_DAYS"; const amount = cadence === "ANNUAL" ? ANNUAL_AMOUNT : MONTHLY_AMOUNT; const shop = session.shop; const shopDomain = (shop || "").split(".")[0]; const returnUrl = `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`; const createRes = await admin.graphql(` mutation CreateSubscription { appSubscriptionCreate( name: "${PLAN_NAME} - ${cadence === "ANNUAL" ? "Annual" : "Monthly"}" returnUrl: "${returnUrl}" lineItems: [{ plan: { appRecurringPricingDetails: { price: { amount: ${amount}, currencyCode: USD } interval: ${interval} } } }] trialDays: ${TRIAL_DAYS} replacementBehavior: STANDARD test: false ) { confirmationUrl appSubscription { id name status trialDays } userErrors { field message } } } `); const data = await createRes.json(); const url = data?.data?.appSubscriptionCreate?.confirmationUrl || null; const userErrors = data?.data?.appSubscriptionCreate?.userErrors || []; const topLevelErrors = data?.errors || []; if (!url || userErrors.length || topLevelErrors.length) { return json({ errors: ["Failed to create subscription.", ...userErrors.map((e) => e.message), ...topLevelErrors.map((e) => e.message || String(e))] }, { status: 400 }); } return json({ confirmationUrl: url }); }; /* ─── NavCard ────────────────────────────────────────────────────────────────── */ function NavCard({ icon, title, desc, link, accent }) { const colors = { blue: { bg: "#eff6ff", border: "#bfdbfe", icon: "#2563eb" }, green: { bg: "#f0fdf4", border: "#bbf7d0", icon: "#16a34a" }, purple: { bg: "#faf5ff", border: "#e9d5ff", icon: "#7c3aed" }, amber: { bg: "#fffbeb", border: "#fde68a", icon: "#b45309" }, }; const c = colors[accent] || colors.blue; return ( { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.08)"; }} onMouseLeave={(e) => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = ""; }} > {icon} {title} {desc} ); } /* ─── Page ───────────────────────────────────────────────────────────────────── */ export default function Index() { const actionData = useActionData(); const loaderData = useLoaderData(); const submit = useSubmit(); const navigate = useNavigate(); const [activeModal, setActiveModal] = useState(false); const [cadence, setCadence] = useState("MONTHLY"); const subscription = loaderData?.subscription || null; const shop = loaderData?.shop || ""; const isSubscribed = loaderData?.isSubscribed || false; const hasConfirmationUrl = Boolean(actionData?.confirmationUrl); const errors = actionData?.errors || []; const shopDomain = (shop || "").split(".")[0]; const formatDate = (d) => d ? new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : "N/A"; const previewBase = subscription?.createdAt ? new Date(subscription.createdAt) : new Date(); const previewTrialEnd = new Date(previewBase); previewTrialEnd.setDate(previewTrialEnd.getDate() + TRIAL_DAYS); 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); const msLeft = trialEnd.getTime() - Date.now(); return Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24))); }, [subscription?.trialDays, subscription?.createdAt, subscription?.status]); const isPreviewMode = !isSubscribed; const displayPlan = isPreviewMode ? `${PLAN_NAME} — ${cadence === "ANNUAL" ? "Annual" : "Monthly"}` : subscription?.name || PLAN_NAME; const displayInterval = isPreviewMode ? (cadence === "ANNUAL" ? "Every 12 months" : "Every 30 days") : getIntervalLabel(subscription?.interval); const displayPrice = isPreviewMode ? (cadence === "ANNUAL" ? `$${ANNUAL_AMOUNT}/yr` : `$${MONTHLY_AMOUNT}/mo`) : (subscription?.priceAmount ? `${formatMoney(subscription.priceAmount, subscription.currencyCode)}${subscription?.interval === "ANNUAL" ? "/yr" : "/mo"}` : "N/A"); const displayNextRenewal = isPreviewMode ? `${formatDate(previewTrialEnd)} (after ${TRIAL_DAYS}-day trial)` : formatDate(subscription?.currentPeriodEnd); const displayStatus = isPreviewMode ? "Not active" : subscription?.status || "N/A"; const statusStyle = getStatusTone(subscription?.status || "PENDING"); const navItems = [ { icon: "⚙️", title: "Settings", desc: "Connect Turn14 API & configure pricing", link: `/app/settings`, accent: "purple" }, { icon: "🏷️", title: "Browse Brands", desc: "Select brands to sync from Turn14", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands`, accent: "blue" }, { icon: "📦", title: "Manage Brands", desc: "Import products from selected brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand`, accent: "green" }, { icon: "📊", title: "Dashboard", desc: "Track live import progress & stats", link: `/app/dashboard`, accent: "amber" }, ]; return ( {/* Hero header */} Data4Autos Turn14 Distribution Integration Sync product brands, manage collections, and automate catalog setup directly from Turn14 into your Shopify store. navigate("/app/dashboard")} style={{ background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: "#fff", padding: "9px 20px", cursor: "pointer", fontWeight: 700, fontSize: 14 }} > 📊 Open Dashboard setActiveModal(true)} style={{ background: isSubscribed ? "rgba(34,197,94,0.2)" : "#2563eb", border: isSubscribed ? "1px solid rgba(34,197,94,0.4)" : "none", borderRadius: 8, color: "#fff", padding: "9px 20px", cursor: "pointer", fontWeight: 700, fontSize: 14 }} > {isSubscribed ? "✅ Subscription Active" : "🚀 Start Free Trial"} {/* Subscription status bar */} Subscription Status {isSubscribed ? subscription?.status : "NOT SUBSCRIBED"} Plan {displayPlan} Price {displayPrice} {trialDaysLeft != null && ( <> Trial Days Left {trialDaysLeft} days > )} setActiveModal(true)} style={{ background: "#f3f4f6", border: "1px solid #e5e7eb", borderRadius: 8, color: "#374151", padding: "7px 16px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}> {isSubscribed ? "View Details" : "Manage Billing →"} {/* Nav cards grid */} {navItems.map((item) => ( ))} {/* Footer help */} Need help? Email support@data4autos.com or visit the Help page {/* Billing modal */} setActiveModal(false)} title="Subscription Details" primaryAction={ hasConfirmationUrl ? undefined : isSubscribed ? undefined : { content: cadence === "ANNUAL" ? `Start ${TRIAL_DAYS}-day Trial — $${ANNUAL_AMOUNT}/yr` : `Start ${TRIAL_DAYS}-day Trial — $${MONTHLY_AMOUNT}/mo`, onAction: () => { const form = document.getElementById("billing-form"); if (form) { const hidden = document.getElementById("cadence-field"); if (hidden) hidden.value = cadence; submit(form, { method: "post" }); } }, } } secondaryActions={[{ content: "Close", onAction: () => setActiveModal(false) }]} > {errors.length > 0 && ( {errors.map((e, i) => {e})} )} {!hasConfirmationUrl && ( <> {subscription?.createdAt && } {!isSubscribed && ( <> Choose your billing plan { 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. setActiveModal(false)} variant="primary" size="large"> Open Billing Confirmation {actionData.confirmationUrl} )} ); }