2026-04-17 21:19:17 +00:00

634 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useMemo, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, useSubmit, Form } from "@remix-run/react";
import {
Page,
Layout,
Card,
BlockStack,
Text,
InlineStack,
Image,
Divider,
Button,
Modal,
TextField,
Box,
Banner,
ChoiceList,
Badge,
} 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";
/* ===========================
PRICING (single source of truth)
=========================== */
const PLAN_NAME = "Starter Sync";
const MONTHLY_AMOUNT = 79; // USD
const ANNUAL_AMOUNT = 790; // USD
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) {
switch (interval) {
case "ANNUAL":
return "Every 12 months";
case "EVERY_30_DAYS":
return "Every 30 days";
default:
return interval || "N/A";
}
}
function getStatusTone(status) {
switch (status) {
case "ACTIVE":
return "success";
case "TRIAL":
return "info";
case "CANCELLED":
case "EXPIRED":
case "DECLINED":
return "critical";
default:
return "attention";
}
}
/* ===========================
LOADER: fetch real subscription details
=========================== */
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;
console.log(
`Loader subscription details for ${shop}: ${JSON.stringify(subscriptionDetails)}`
);
if (shop === "racewerksengg.myshopify.com") {
return json({
redirectToBilling: false,
shop,
isSubscribed: true,
subscription: subscriptionDetails,
allSubscriptions: subscriptions,
});
}
return json({
redirectToBilling: !isSubscribed,
shop,
isSubscribed,
subscription: subscriptionDetails,
allSubscriptions: subscriptions,
});
};
/* ===========================
ACTION: create subscription
=========================== */
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 });
};
/* ===========================
PAGE
=========================== */
export default function Index() {
const actionData = useActionData();
const loaderData = useLoaderData();
const submit = useSubmit();
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 items = [
{
icon: "⚙️",
text: "Manage API settings",
link: `/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 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) 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?.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 — will be created at checkout"
: subscription?.status || "N/A";
return (
<Page>
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<Layout.Section>
<Card padding="500">
<BlockStack gap="400">
<BlockStack gap="400" align="center">
<Text variant="headingLg" as="h1" alignment="center">
Welcome to your Turn14 Dashboard
</Text>
<InlineStack gap="800" align="center" blockAlign="center">
<Image source={data4autosLogo} alt="Data4Autos Logo" width={120} />
<Image
source={turn14DistributorLogo}
alt="Turn14 Distributors Logo"
width={200}
/>
</InlineStack>
</BlockStack>
<Divider />
<BlockStack gap="500">
<InlineStack align="space-between" blockAlign="center">
<Text variant="headingMd" as="h3">
Subscription Overview
</Text>
<Badge tone={getStatusTone(subscription?.status || "PENDING")}>
{isSubscribed ? subscription?.status : "NOT SUBSCRIBED"}
</Badge>
</InlineStack>
<Box
padding="400"
background="bg-surface-secondary"
borderWidth="025"
borderRadius="200"
>
<BlockStack gap="300">
<Text as="p">
<strong>Plan:</strong> {displayPlan}
</Text>
<Text as="p">
<strong>Billing:</strong> {displayInterval}
</Text>
<Text as="p">
<strong>Price:</strong> {displayPrice}
</Text>
<Text as="p">
<strong>Next renewal / period end:</strong> {displayNextRenewal}
</Text>
<Text as="p">
<strong>Trial:</strong>{" "}
{isSubscribed
? `${subscription?.trialDays || 0} day(s)`
: `${TRIAL_DAYS} day free trial`}
</Text>
<Text as="p">
<strong>Trial days left:</strong>{" "}
{trialDaysLeft != null ? `${trialDaysLeft} day(s)` : "N/A"}
</Text>
</BlockStack>
</Box>
</BlockStack>
<Divider />
<BlockStack gap="800">
<Text variant="headingMd" as="h3">
🚀 Data4Autos Turn14 Integration gives you the power to sync
product brands, manage collections, and automate catalog setup
directly from Turn14 to your Shopify store.
</Text>
<InlineStack gap="400">
<Text as="h3" variant="headingLg" fontWeight="medium">
Use the left sidebar to:
</Text>
<Box
paddingBlockStart="800"
paddingBlockEnd="800"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1rem",
}}
>
{items.map((item, index) => (
<Card key={index} padding="500" background="bg-surface-secondary">
<BlockStack align="center" gap="200">
<Text
as="p"
fontWeight="bold"
alignment="center"
tone="subdued"
variant="bodyMd"
>
<span style={{ fontSize: "2rem" }}>{item.icon}</span>
</Text>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Text
as="h6"
alignment="center"
fontWeight="bold"
variant="headingMd"
>
{item.text}
</Text>
</a>
</BlockStack>
</Card>
))}
</Box>
</InlineStack>
</BlockStack>
<Divider />
<BlockStack gap="400">
<Text tone="subdued" alignment="center">
Need help? Contact us at{" "}
<a href="mailto:support@data4autos.com">support@data4autos.com</a>
</Text>
<Button size="large" variant="primary" onClick={openModal} fullWidth>
{loaderData?.redirectToBilling
? "Proceed to Billing"
: "View Subscription Details"}
</Button>
</BlockStack>
</BlockStack>
</Card>
</Layout.Section>
</Layout>
<Modal
open={activeModal}
onClose={closeModal}
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: closeModal }]}
>
<Form id="billing-form" method="post">
<input type="hidden" name="cadence" id="cadence-field" value={cadence} readOnly />
<Modal.Section>
<BlockStack gap="300">
{errors.length > 0 && (
<Banner title="Couldnt create subscription" tone="critical">
<ul style={{ margin: 0, paddingLeft: "1rem" }}>
{errors.map((e, i) => (
<li key={i}>{e}</li>
))}
</ul>
</Banner>
)}
{!hasConfirmationUrl && (
<>
<TextField label="Subscription Status" value={displayStatus} readOnly />
<TextField label="Plan" value={displayPlan} readOnly />
<TextField label="Billing Interval" value={displayInterval} readOnly />
<TextField label="Price" value={displayPrice} readOnly />
<TextField
label="Trial"
value={
isSubscribed
? `${subscription?.trialDays || 0} day(s)`
: `${TRIAL_DAYS}-day free trial`
}
readOnly
/>
<TextField
label="Trial Days Left"
value={trialDaysLeft != null ? `${trialDaysLeft} days left` : "N/A"}
readOnly
/>
<TextField
label="Next Renewal / Period End"
value={displayNextRenewal}
readOnly
/>
{subscription?.createdAt && (
<TextField
label="Subscription Created"
value={formatDate(subscription.createdAt)}
readOnly
/>
)}
<Divider />
{!isSubscribed && (
<>
<Text as="h3" variant="headingMd">
Choose your billing plan
</Text>
<ChoiceList
title="Choose your billing cadence"
choices={[
{
label: `Monthly — $${MONTHLY_AMOUNT}/mo`,
value: "MONTHLY",
helpText:
"Flexible monthly billing. Cancel anytime during or after the trial.",
},
{
label: `Annual — $${ANNUAL_AMOUNT}/yr (save ~17%)`,
value: "ANNUAL",
helpText: "Best value. Billed annually after your free trial ends.",
},
]}
selected={[cadence]}
onChange={(selected) => {
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 && (
<BlockStack gap="300">
<Banner title="Almost there!" tone="success">
Click the button below to open Shopifys billing confirmation in a
new tab.
</Banner>
<Button
url={actionData.confirmationUrl}
target="_blank"
external
onClick={() => setActiveModal(false)}
variant="primary"
size="large"
>
Open Billing Confirmation
</Button>
<a
href={actionData.confirmationUrl}
target="_blank"
rel="noopener noreferrer"
style={{ wordBreak: "break-all" }}
>
{actionData.confirmationUrl}
</a>
</BlockStack>
)}
</BlockStack>
</Modal.Section>
</Form>
</Modal>
</Page>
);
}