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 navigate = useNavigate(); 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 (