348 lines
12 KiB
JavaScript
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>
|
|
);
|
|
}
|