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} )}
); }