378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
// app/routes/app.billing.success.jsx
|
||
import React, { useMemo } from "react";
|
||
import { json } from "@remix-run/node";
|
||
import { useLoaderData } from "@remix-run/react";
|
||
import {
|
||
Page,
|
||
Layout,
|
||
Card,
|
||
BlockStack,
|
||
InlineStack,
|
||
Text,
|
||
Badge,
|
||
Divider,
|
||
Button,
|
||
Banner,
|
||
Box,
|
||
} from "@shopify/polaris";
|
||
import { TitleBar } from "@shopify/app-bridge-react";
|
||
import { authenticate } from "../shopify.server";
|
||
|
||
/** ===========================
|
||
* LOADER
|
||
* =========================== */
|
||
export const loader = async ({ request }) => {
|
||
const { admin, session } = await authenticate.admin(request);
|
||
const shop = session.shop;
|
||
const shopDomain = shop.split(".")[0];
|
||
|
||
// Pull richer subscription details: interval + price
|
||
const resp = await admin.graphql(`
|
||
query ActiveSubForSuccess {
|
||
currentAppInstallation {
|
||
activeSubscriptions {
|
||
id
|
||
name
|
||
status
|
||
trialDays
|
||
createdAt
|
||
currentPeriodEnd
|
||
test
|
||
lineItems {
|
||
plan {
|
||
appRecurringPricingDetails {
|
||
interval
|
||
price { amount currencyCode }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`);
|
||
|
||
const result = await resp.json();
|
||
const subscription =
|
||
result?.data?.currentAppInstallation?.activeSubscriptions?.[0] || null;
|
||
|
||
// Detect recent activation (today or last 2 days)
|
||
let recentActivation = false;
|
||
if (subscription?.createdAt) {
|
||
const created = new Date(subscription.createdAt);
|
||
const now = new Date();
|
||
const diffMs = now.getTime() - created.getTime();
|
||
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
|
||
recentActivation =
|
||
diffMs >= 0 &&
|
||
diffMs <= TWO_DAYS_MS &&
|
||
(subscription.status === "ACTIVE" || subscription.status === "TRIAL");
|
||
}
|
||
|
||
return json({ subscription, shop, shopDomain, recentActivation });
|
||
};
|
||
|
||
/** ===========================
|
||
* HELPERS
|
||
* =========================== */
|
||
function formatDate(d) {
|
||
if (!d) return "N/A";
|
||
return new Date(d).toLocaleString(undefined, {
|
||
year: "numeric",
|
||
month: "short",
|
||
day: "numeric",
|
||
});
|
||
}
|
||
|
||
function getRecurringLine(subscription) {
|
||
const items = subscription?.lineItems || [];
|
||
for (const li of items) {
|
||
const r = li?.plan?.appRecurringPricingDetails;
|
||
if (r) return r;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function intervalToLabel(interval) {
|
||
switch (interval) {
|
||
case "ANNUAL":
|
||
return "Annual";
|
||
case "EVERY_30_DAYS":
|
||
return "Monthly";
|
||
default:
|
||
return interval || "N/A";
|
||
}
|
||
}
|
||
|
||
/** ===========================
|
||
* PAGE
|
||
* =========================== */
|
||
export default function BillingSuccess() {
|
||
const { subscription, shop, shopDomain, recentActivation } = useLoaderData();
|
||
|
||
const recurring = getRecurringLine(subscription);
|
||
const cadenceLabel = intervalToLabel(recurring?.interval);
|
||
const priceText =
|
||
recurring?.price?.amount && recurring?.price?.currencyCode
|
||
? `${recurring.price.amount} ${recurring.price.currencyCode}`
|
||
: "N/A";
|
||
|
||
// Trial end
|
||
const trialEndStr = useMemo(() => {
|
||
if (!subscription?.trialDays || !subscription?.createdAt) return "N/A";
|
||
const start = new Date(subscription.createdAt);
|
||
const end = new Date(start);
|
||
end.setDate(end.getDate() + subscription.trialDays);
|
||
return formatDate(end);
|
||
}, [subscription?.trialDays, subscription?.createdAt]);
|
||
|
||
// Trial days left (if still on TRIAL)
|
||
const trialDaysLeft = useMemo(() => {
|
||
if (
|
||
subscription?.status !== "TRIAL" ||
|
||
!subscription?.trialDays ||
|
||
!subscription?.createdAt
|
||
)
|
||
return null;
|
||
const start = new Date(subscription.createdAt);
|
||
const trialEnd = new Date(start);
|
||
trialEnd.setDate(trialEnd.getDate() + subscription.trialDays);
|
||
const now = new Date();
|
||
const left = Math.ceil(
|
||
(trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
||
);
|
||
return Math.max(0, left);
|
||
}, [subscription?.status, subscription?.trialDays, subscription?.createdAt]);
|
||
|
||
const showError = !subscription;
|
||
const nextRenewal = formatDate(subscription?.currentPeriodEnd);
|
||
const createdAt = formatDate(subscription?.createdAt);
|
||
|
||
return (
|
||
<Page>
|
||
<TitleBar title="Payment & Subscription" />
|
||
<Layout>
|
||
<Layout.Section>
|
||
{showError ? (
|
||
<Banner title="No active subscription found" tone="critical">
|
||
<p>
|
||
We couldn’t find an active subscription for this shop. If you just
|
||
approved billing, it may not be visible yet. You can proceed to the
|
||
billing screen to create or refresh your subscription.
|
||
</p>
|
||
<InlineStack gap="400" wrap={false}>
|
||
<Button url={`/app`} variant="primary">
|
||
Go to Dashboard
|
||
</Button>
|
||
<Button url={`/app`} variant="secondary">
|
||
Proceed to Billing
|
||
</Button>
|
||
</InlineStack>
|
||
</Banner>
|
||
) : recentActivation ? (
|
||
<Banner title="🎉 Subscription Activated" tone="success">
|
||
<p>
|
||
Congratulations! Your plan is now active. You’re all set to sync brands,
|
||
build collections, and automate your Turn14 catalog.
|
||
</p>
|
||
<p style={{ marginTop: 8 }}>
|
||
Activated: <strong>{createdAt}</strong> • Status:{" "}
|
||
<Badge tone="success">{subscription.status}</Badge>
|
||
</p>
|
||
<InlineStack gap="400" wrap={false}>
|
||
<Button
|
||
url={`https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`}
|
||
target="_blank"
|
||
external
|
||
variant="primary"
|
||
>
|
||
Open App Dashboard
|
||
</Button>
|
||
<Button
|
||
url={`https://admin.shopify.com/store/${shopDomain}/settings/billing`}
|
||
target="_blank"
|
||
external
|
||
variant="secondary"
|
||
>
|
||
View Billing & Invoices
|
||
</Button>
|
||
</InlineStack>
|
||
</Banner>
|
||
) : (
|
||
<Banner title="Your plan is active" tone="info">
|
||
<p>
|
||
Your subscription is active. Below are the full details of your plan,
|
||
trial, and renewal.
|
||
</p>
|
||
</Banner>
|
||
)}
|
||
</Layout.Section>
|
||
|
||
<Layout.Section>
|
||
<Card padding="500">
|
||
<BlockStack gap="500">
|
||
<Text variant="headingLg" as="h2">
|
||
Plan Overview
|
||
</Text>
|
||
|
||
<Box
|
||
padding="400"
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||
gap: "1rem",
|
||
}}
|
||
>
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="200">
|
||
<Text as="p" tone="subdued">
|
||
Plan Name
|
||
</Text>
|
||
<Text as="p" variant="headingMd" fontWeight="bold">
|
||
{subscription?.name || "Starter Sync"}
|
||
</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="200">
|
||
<Text as="p" tone="subdued">
|
||
Billing Cadence
|
||
</Text>
|
||
<Text as="p" variant="headingMd" fontWeight="bold">
|
||
{cadenceLabel}
|
||
</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="200">
|
||
<Text as="p" tone="subdued">
|
||
Price
|
||
</Text>
|
||
<Text as="p" variant="headingMd" fontWeight="bold">
|
||
{priceText}
|
||
</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
<Text variant="headingMd" as="h3">
|
||
Billing & Trial
|
||
</Text>
|
||
<Box
|
||
padding="400"
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
|
||
gap: "1rem",
|
||
}}
|
||
>
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Status</Text>
|
||
<InlineStack gap="200">
|
||
<Badge
|
||
tone={subscription?.status === "ACTIVE" ? "success" : "attention"}
|
||
>
|
||
{subscription?.status || "N/A"}
|
||
</Badge>
|
||
{subscription?.test && <Badge tone="warning">Test</Badge>}
|
||
</InlineStack>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Trial</Text>
|
||
<Text fontWeight="bold">
|
||
{subscription?.trialDays ? `${subscription.trialDays} days` : "N/A"}
|
||
{trialDaysLeft != null && <span> — {trialDaysLeft} day(s) left</span>}
|
||
</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Trial Ends</Text>
|
||
<Text fontWeight="bold">{trialEndStr}</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Next Renewal / Period End</Text>
|
||
<Text fontWeight="bold">{nextRenewal}</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
<Text variant="headingMd" as="h3">
|
||
Subscription Metadata
|
||
</Text>
|
||
<Box
|
||
padding="400"
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||
gap: "1rem",
|
||
}}
|
||
>
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Subscription ID</Text>
|
||
<Text fontWeight="bold" breakWord>
|
||
{subscription?.id || "N/A"}
|
||
</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Created / Activated</Text>
|
||
<Text fontWeight="bold">{createdAt}</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
|
||
<Card padding="400" background="bg-surface-secondary">
|
||
<BlockStack gap="150">
|
||
<Text tone="subdued">Shop</Text>
|
||
<Text fontWeight="bold">{shop}</Text>
|
||
</BlockStack>
|
||
</Card>
|
||
</Box>
|
||
|
||
<Divider />
|
||
|
||
<InlineStack gap="400" align="center">
|
||
<Button
|
||
url={`https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`}
|
||
target="_blank"
|
||
external
|
||
variant="primary"
|
||
>
|
||
Go to App Dashboard
|
||
</Button>
|
||
<Button
|
||
url={`https://admin.shopify.com/store/${shopDomain}/settings/billing`}
|
||
target="_blank"
|
||
external
|
||
variant="secondary"
|
||
>
|
||
Manage Billing / Invoices
|
||
</Button>
|
||
<Button url="mailto:support@data4autos.com" variant="tertiary">
|
||
Contact Support
|
||
</Button>
|
||
</InlineStack>
|
||
</BlockStack>
|
||
</Card>
|
||
</Layout.Section>
|
||
</Layout>
|
||
</Page>
|
||
);
|
||
}
|