// 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 (
Data4Autos Turn14 Integration
Shop: {shopName} {/* —— TURN14 FORM —— */}
{actionData?.error && !actionData?.pricingSaved && ( )} {(actionData?.success || Boolean(savedCreds.accessToken)) && (

✅ Turn14 connected successfully!

)} {/* —— PRICING CONFIG (direct save via this route) —— */} {(actionData?.success || Boolean(savedCreds.accessToken)) && (