2025-08-29 02:47:12 +00:00

348 lines
12 KiB
JavaScript

// app/routes/store-credentials.jsx
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
import { useEffect, useMemo, useState } from "react";
import {
Page,
Layout,
Card,
TextField,
Button,
TextContainer,
InlineError,
Text,
BlockStack,
Box,
Select,
Banner,
InlineStack,
} from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react";
import { authenticate } from "../shopify.server";
const SCOPES = [
"read_inventory",
"read_products",
"write_inventory",
"write_products",
"read_publications",
"write_publications",
"read_fulfillments",
"write_fulfillments","read_locations","write_locations"
].join(",");
const REDIRECT_URI = "https://backend.data4autos.com/auth/callback";
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
// ===== LOADER =====
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const resp = await admin.graphql(`
{
shop {
id
name
metafield(namespace: "turn14", key: "credentials") { value }
pricing: metafield(namespace: "turn14", key: "pricing_config") { value }
}
}
`);
const { data } = await resp.json();
let creds = {};
if (data.shop.metafield?.value) {
try { creds = JSON.parse(data.shop.metafield.value); } catch { }
}
creds = {};
let savedPricing = { priceType: "map", percentage: 0 };
if (data.shop.pricing?.value) {
try {
const p = JSON.parse(data.shop.pricing.value);
savedPricing.priceType = (p.priceType || "map").toLowerCase();
savedPricing.percentage = Number(p.percentage) || 0;
} catch { }
}
return json({
shopName: data.shop.name,
shopId: data.shop.id,
savedCreds: creds,
savedPricing,
});
};
// ===== ACTION =====
export const action = async ({ request }) => {
const formData = await request.formData();
const intent = formData.get("intent"); // "connect_turn14" | "save_pricing"
const { admin } = await authenticate.admin(request);
// we need shop id either way
const shopResp = await admin.graphql(`{ shop { id name } }`);
const shopJson = await shopResp.json();
const shopId = shopJson.data.shop.id;
const shopName = shopJson.data.shop.name;
if (intent === "save_pricing") {
// --- save pricing_config metafield directly ---
const priceTypeRaw = (formData.get("price_type") || "map").toString().toLowerCase();
const percentageRaw = Number(formData.get("percentage") || 0);
const priceType = ["map", "percentage"].includes(priceTypeRaw) ? priceTypeRaw : "map";
const percentage = Number.isFinite(percentageRaw) ? percentageRaw : 0;
const cfg = { priceType, percentage };
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "pricing_config",
type: "json",
value: "${JSON.stringify(cfg).replace(/"/g, '\\"')}"
}]) {
userErrors { message }
}
}
`;
const saveRes = await admin.graphql(mutation);
const saveJson = await saveRes.json();
const errs = saveJson.data.metafieldsSet.userErrors;
if (errs.length) {
return json({ success: false, pricingSaved: false, error: errs[0].message });
}
return json({ success: true, pricingSaved: true, savedPricing: cfg });
}
// default / legacy: connect Turn14 flow
// const clientId = formData.get("client_id");
// const clientSecret = formData.get("client_secret");
const clientId = formData.get("demo_client_id");
const clientSecret = formData.get("demo_client_secret");
let tokenData;
try {
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
}),
});
tokenData = await tokenRes.json();
if (!tokenRes.ok) {
throw new Error(tokenData.error || "Failed to fetch Turn14 token");
}
} catch (err) {
return json({ success: false, error: err.message });
}
const creds = {
clientId,
clientSecret,
accessToken: tokenData.access_token,
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
};
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "credentials",
type: "json",
value: "${JSON.stringify(creds).replace(/"/g, '\\"')}"
}]) {
userErrors { message }
}
}
`;
const saveRes = await admin.graphql(mutation);
const saveJson = await saveRes.json();
const errs = saveJson.data.metafieldsSet.userErrors;
if (errs.length) {
return json({ success: false, error: errs[0].message });
}
const stateNonce = Math.random().toString(36).slice(2);
const installUrl =
`https://${shopName}.myshopify.com/admin/oauth/authorize` +
`?client_id=${CLIENT_ID}` +
`&scope=${SCOPES}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&state=${stateNonce}`;
return json({
success: true,
confirmationUrl: installUrl,
creds,
});
};
// ===== COMPONENT =====
export default function StoreCredentials() {
const { shopName, savedCreds, savedPricing } = useLoaderData();
const actionData = useActionData();
// open Shopify install after Connect Turn14
useEffect(() => {
if (actionData?.confirmationUrl) {
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
}
}, [actionData?.confirmationUrl]);
const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || "");
const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || "");
const connected = actionData?.success || Boolean(savedCreds.accessToken);
// Pricing UI state (seed from loader or last action)
const initialPriceType = useMemo(
() => (actionData?.savedPricing?.priceType || savedPricing?.priceType || "map"),
[actionData?.savedPricing?.priceType, savedPricing?.priceType]
);
const initialPercentage = useMemo(
() => Number(actionData?.savedPricing?.percentage ?? savedPricing?.percentage ?? 0),
[actionData?.savedPricing?.percentage, savedPricing?.percentage]
);
const [priceType, setPriceType] = useState(initialPriceType);
const [percentage, setPercentage] = useState(initialPercentage);
const pricingSavedOk = actionData?.pricingSaved && actionData?.success && !actionData?.error;
const pricingError = actionData?.pricingSaved === false ? actionData?.error : null;
return (
<Page>
<TitleBar title="Turn14 & Shopify Connect" />
<div style={{ display: "flex", justifyContent: "center", textAlign: "center", paddingBottom: "20px" }}>
<Text as="h1" variant="headingLg">Data4Autos Turn14 Integration</Text>
</div>
<Layout>
<Layout.Section>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<Box maxWidth="520px" width="100%" marginInline="auto">
<Card sectioned padding="600">
<BlockStack gap="400">
<TextContainer spacing="tight">
<Text variant="headingLg" align="center" fontWeight="medium">Shop: {shopName}</Text>
</TextContainer>
{/* —— TURN14 FORM —— */}
<Form method="post">
<input type="hidden" name="intent" value="connect_turn14" />
<input type="hidden" name="demo_client_id" value="671f15b6973625885ee392122113d9ed54103ddd" />
<input type="hidden" name="demo_client_secret" value="98907c3b3d92235c99338bd0ce307144b2f66874" />
<BlockStack gap="400">
<BlockStack gap="200">
<TextField
label="Turn14 Client ID"
name="client_id"
// value={clientId}
value={"********************************************************"}
onChange={setClientId}
autoComplete="off"
// requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<TextField
label="Turn14 Client Secret"
name="client_secret"
// value={clientSecret}
value={"********************************************************"}
onChange={setClientSecret}
autoComplete="off"
// requiredIndicator
padding="200"
/>
</BlockStack>
<BlockStack gap="200">
<Button submit primary size="large" variant="primary">
Connect Turn14 With Demo Credentials
</Button>
</BlockStack>
<BlockStack gap="200">
<Button submit primary size="large" variant="primary">
Connect Turn14
</Button>
</BlockStack>
</BlockStack>
</Form>
{actionData?.error && !actionData?.pricingSaved && (
<TextContainer spacing="tight" style={{ marginTop: "1rem" }}>
<InlineError message={actionData.error} fieldID="client_id" />
</TextContainer>
)}
{(actionData?.success || Boolean(savedCreds.accessToken)) && (
<TextContainer spacing="tight" style={{ marginTop: "1.5rem" }}>
<p style={{ color: "green", paddingTop: "5px" }}> Turn14 connected successfully!</p>
</TextContainer>
)}
{/* —— PRICING CONFIG (direct save via this route) —— */}
{(actionData?.success || Boolean(savedCreds.accessToken)) && (
<Card title="Pricing configuration" sectioned>
<BlockStack gap="400">
<Form method="post">
<input type="hidden" name="intent" value="save_pricing" />
<Select
label="Price type"
options={[
{ label: "MAP (no change)", value: "map" },
{ label: "MAP + % profit", value: "percentage" },
]}
value={priceType}
onChange={(val) => setPriceType(val)}
name="price_type"
/>
{priceType === "percentage" && (
<TextField
type="number"
label="Percentage"
helpText="Add this percentage on top of MAP."
value={String(percentage)}
onChange={(val) => setPercentage(val)}
autoComplete="off"
suffix="%"
min={0}
name="percentage"
/>
)}
<div style={{paddingTop:"15px", textAlign:"end"}}>
<Button submit primary variant="primary" size="large" >Save pricing</Button>
</div>
</Form>
{pricingSavedOk && (
<Banner tone="success">
<p>Pricing configuration saved.</p>
</Banner>
)}
{pricingError && (
<Banner tone="critical">
<p>{pricingError}</p>
</Banner>
)}
</BlockStack>
</Card>
)}
</BlockStack>
</Card>
</Box>
</div>
</Layout.Section>
</Layout>
</Page>
);
}