+
- Brand: {brand.name}
ID: {brand.id}
-
-
-
+
{expandedBrand === brand.id && (
-
-
-
+
@@ -260,7 +395,7 @@ export default function ManageBrandProducts() {
) : (
- {(itemsMap[brand.id] || []).map(item => (
+ {(itemsMap[brand.id] || []).map((item) => (
@@ -276,9 +411,7 @@ export default function ManageBrandProducts() {
Part Number: {item.attributes.part_number}
- Brand: {item.attributes.brand}
- Category: {item.attributes.category} > {item.attributes.subcategory}
- Dimensions: {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in
+ Category: {item.attributes.category} > {item.attributes.subcategory}
@@ -289,9 +422,203 @@ export default function ManageBrandProducts() {
)}
-
+
))}
);
}
+ */
+import React, { useState } from "react";
+import { json } from "@remix-run/node";
+import { useLoaderData, Form, useActionData } from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ IndexTable,
+ Card,
+ Thumbnail,
+ TextContainer,
+ Spinner,
+ Button,
+ TextField,
+ Banner,
+} from "@shopify/polaris";
+import { authenticate } from "../shopify.server";
+import { TitleBar } from "@shopify/app-bridge-react";
+
+export const loader = async ({ request }) => {
+ const { admin } = await authenticate.admin(request);
+ const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
+ const accessToken = await getTurn14AccessTokenFromMetafield(request);
+
+ const res = await admin.graphql(`{
+ shop {
+ metafield(namespace: "turn14", key: "selected_brands") {
+ value
+ }
+ }
+ }`);
+ const data = await res.json();
+ const rawValue = data?.data?.shop?.metafield?.value;
+
+ let brands = [];
+ try {
+ brands = JSON.parse(rawValue);
+ } catch (err) {
+ console.error("❌ Failed to parse metafield value:", err);
+ }
+
+ return json({ brands, accessToken });
+};
+
+export default function ManageBrandProducts() {
+ const actionData = useActionData();
+ const { brands, accessToken } = useLoaderData();
+ const [expandedBrand, setExpandedBrand] = useState(null);
+ const [itemsMap, setItemsMap] = useState({});
+ const [loadingMap, setLoadingMap] = useState({});
+ const [productCount, setProductCount] = useState("10");
+
+ const toggleBrandItems = async (brandId) => {
+ const isExpanded = expandedBrand === brandId;
+ if (isExpanded) {
+ setExpandedBrand(null);
+ } else {
+ setExpandedBrand(brandId);
+ if (!itemsMap[brandId]) {
+ setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
+ try {
+ const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ const data = await res.json();
+ setItemsMap((prev) => ({ ...prev, [brandId]: data }));
+ } catch (err) {
+ console.error("Error fetching items:", err);
+ }
+ setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
+ }
+ }
+ };
+
+ return (
+
+
+
+ {brands.length === 0 ? (
+
+
+ No brands selected yet.
+
+
+ ) : (
+
+
+
+ {brands.map((brand, index) => (
+
+ {brand.id}
+
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {brands.map(
+ (brand) =>
+ expandedBrand === brand.id && (
+
+
+ {actionData?.success && (
+
+
+ {actionData.results.map((r) => (
+
+ Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
+
+ ))}
+
+
+ )}
+
+
+
+
+ {loadingMap[brand.id] ? (
+
+ ) : (
+
+ {(itemsMap[brand.id] || []).map((item) => (
+
+
+
+
+
+
+
+ Part Number: {item.attributes.part_number}
+ Category: {item.attributes.category} > {item.attributes.subcategory}
+
+
+
+
+ ))}
+
+ )}
+
+
+ )
+ )}
+
+
+ );
+}
diff --git a/app/routes/app.managebrand1.jsx b/app/routes/app.managebrand1.jsx
deleted file mode 100644
index 9a9c280..0000000
--- a/app/routes/app.managebrand1.jsx
+++ /dev/null
@@ -1,429 +0,0 @@
-import React, { useState } from "react";
-import { json } from "@remix-run/node";
-import { useLoaderData, Form, useActionData } from "@remix-run/react";
-import {
- Page,
- Layout,
- Card,
- Thumbnail,
- TextContainer,
- Spinner,
- Button,
- TextField,
- Banner,
- InlineError,
-} from "@shopify/polaris";
-import { authenticate } from "../shopify.server";
-
-// Load selected brands and access token from Shopify metafield
-export const loader = async ({ request }) => {
- const { admin } = await authenticate.admin(request);
- const { getTurn14AccessTokenFromMetafield } = await import(
- "../utils/turn14Token.server"
- );
- const accessToken = await getTurn14AccessTokenFromMetafield(request);
-
- const res = await admin.graphql(`
- {
- shop {
- metafield(namespace: "turn14", key: "selected_brands") {
- value
- }
- }
- }
- `);
- const data = await res.json();
- const rawValue = data?.data?.shop?.metafield?.value;
-
- let brands = [];
- try {
- brands = JSON.parse(rawValue);
- } catch (err) {
- console.error("❌ Failed to parse metafield value:", err);
- }
-
- return json({ brands, accessToken });
-};
-
-// Handle adding products for a specific brand
-export const action = async ({ request }) => {
- const { admin } = await authenticate.admin(request);
- const formData = await request.formData();
- const brandId = formData.get("brandId");
- const rawCount = formData.get("productCount");
- const productCount = parseInt(rawCount, 10) || 10;
-
- const { getTurn14AccessTokenFromMetafield } = await import(
- "../utils/turn14Token.server"
- );
- const accessToken = await getTurn14AccessTokenFromMetafield(request);
-
- // Fetch items from Turn14 API
- const itemsRes = await fetch(
- `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
- {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- }
- );
- const itemsData = await itemsRes.json();
-
- function slugify(str) {
- return str
- .toString()
- .trim()
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, '-')
- .replace(/^-+|-+$/g, '');
- }
-
- const items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
- const results = [];
- for (const item of items1) {
- const attrs = item.attributes;
-
- // 0️⃣ Build and normalize collection titles
- const category = attrs.category;
- const subcategory = attrs.subcategory || "";
- const brand = attrs.brand;
- const subcats = subcategory
- .split(/[,\/]/)
- .map((s) => s.trim())
- .filter(Boolean);
- const collectionTitles = Array.from(
- new Set([category, ...subcats, brand].filter(Boolean))
- );
-
- // 1️⃣ Find or create collections, collect their IDs
- const collectionIds = [];
- for (const title of collectionTitles) {
- // lookup
- const lookupRes = await admin.graphql(`
- {
- collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
- nodes { id }
- }
- }
- `);
- const lookupJson = await lookupRes.json();
- const existing = lookupJson.data.collections.nodes;
- if (existing.length) {
- collectionIds.push(existing[0].id);
- } else {
- // create
- const createColRes = await admin.graphql(`
- mutation($input: CollectionInput!) {
- collectionCreate(input: $input) {
- collection { id }
- userErrors { field message }
- }
- }
- `, { variables: { input: { title } } });
- const createColJson = await createColRes.json();
- const errs = createColJson.data.collectionCreate.userErrors;
- if (errs.length) {
- throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
- }
- collectionIds.push(createColJson.data.collectionCreate.collection.id);
- }
- }
-
- // 2️⃣ Build tags
- const tags = [
- attrs.category,
- ...subcats,
- attrs.brand,
- attrs.part_number,
- attrs.mfr_part_number,
- attrs.price_group,
- attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
- attrs.barcode
- ].filter(Boolean).map((t) => t.trim());
-
- // 3️⃣ Prepare media inputs
- const mediaInputs = (attrs.files || [])
- .filter((f) => f.type === "Image" && f.url)
- .map((file) => ({
- originalSource: file.url,
- mediaContentType: "IMAGE",
- alt: `${attrs.product_name} — ${file.media_content}`,
- }));
-
-
- // 2️⃣ Pick the longest “Market Description” or fallback to part_description
- const marketDescs = (attrs.descriptions || [])
- .filter((d) => d.type === "Market Description")
- .map((d) => d.description);
- const descriptionHtml = marketDescs.length
- ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
- : attrs.part_description;
-
- // 4️⃣ Create product + attach to collections + add media
- const createProdRes = await admin.graphql(`
- mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
- productCreate(product: $prod, media: $media) {
- product {
- id
- variants(first: 1) {
- nodes { id inventoryItem { id } }
- }
- }
- userErrors { field message }
- }
- }
- `, {
- variables: {
- prod: {
- title: attrs.product_name,
- descriptionHtml: descriptionHtml,
- vendor: attrs.brand,
- productType: attrs.category,
- handle: slugify(attrs.part_number || attrs.product_name),
- tags,
- collectionsToJoin: collectionIds,
- status: "ACTIVE",
- },
- media: mediaInputs,
- },
- });
- const createProdJson = await createProdRes.json();
- const prodErrs = createProdJson.data.productCreate.userErrors;
- if (prodErrs.length) {
- const taken = prodErrs.some((e) => /already in use/i.test(e.message));
- if (taken) {
- results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
- continue;
- }
- throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
- }
-
- const product = createProdJson.data.productCreate.product;
- const variantNode = product.variants.nodes[0];
- const variantId = variantNode.id;
- const inventoryItemId = variantNode.inventoryItem.id;
-
- // 5️⃣ Bulk-update variant (price, compare-at, barcode)
- const price = parseFloat(attrs.price) || 1000;
- const comparePrice = parseFloat(attrs.compare_price) || null;
- const barcode = attrs.barcode || "";
-
- const bulkRes = await admin.graphql(`
- mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
- productVariantsBulkUpdate(productId: $productId, variants: $variants) {
- productVariants { id price compareAtPrice barcode }
- userErrors { field message }
- }
- }
- `, {
- variables: {
- productId: product.id,
- variants: [{
- id: variantId,
- price,
- ...(comparePrice !== null && { compareAtPrice: comparePrice }),
- ...(barcode && { barcode }),
- }],
- },
- });
- const bulkJson = await bulkRes.json();
- const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
- if (bulkErrs.length) {
- throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
- }
- const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0];
-
- // 6️⃣ Update inventory item (SKU, cost & weight)
- const costPerItem = parseFloat(attrs.purchase_cost) || 0;
- const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
-
- const invRes = await admin.graphql(`
- mutation($id: ID!, $input: InventoryItemInput!) {
- inventoryItemUpdate(id: $id, input: $input) {
- inventoryItem {
- id
- sku
- measurement {
- weight { value unit }
- }
- }
- userErrors { field message }
- }
- }
- `, {
- variables: {
- id: inventoryItemId,
- input: {
- sku: attrs.part_number,
- cost: costPerItem,
- measurement: {
- weight: { value: weightValue, unit: "POUNDS" }
- },
- },
- },
- });
- const invJson = await invRes.json();
- const invErrs = invJson.data.inventoryItemUpdate.userErrors;
- if (invErrs.length) {
- throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
- }
- const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
-
- // 7️⃣ Collect results
- results.push({
- productId: product.id,
- variant: {
- id: updatedVariant.id,
- price: updatedVariant.price,
- compareAtPrice: updatedVariant.compareAtPrice,
- sku: inventoryItem.sku,
- barcode: updatedVariant.barcode,
- weight: inventoryItem.measurement.weight.value,
- weightUnit: inventoryItem.measurement.weight.unit,
- },
- collections: collectionTitles,
- tags,
- });
- }
-
-
-
-
- return json({ success: true, results });
-};
-
-// Main React component for managing brand products
-export default function ManageBrandProducts() {
- const actionData = useActionData();
- const { brands, accessToken } = useLoaderData();
- const [expandedBrand, setExpandedBrand] = useState(null);
- const [itemsMap, setItemsMap] = useState({});
- const [loadingMap, setLoadingMap] = useState({});
- const [productCount, setProductCount] = useState("10");
-
- const toggleBrandItems = async (brandId) => {
- const isExpanded = expandedBrand === brandId;
- if (isExpanded) {
- setExpandedBrand(null);
- } else {
- setExpandedBrand(brandId);
- if (!itemsMap[brandId]) {
- setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
- try {
- const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/json",
- },
- });
- const data = await res.json();
- setItemsMap((prev) => ({ ...prev, [brandId]: data }));
- } catch (err) {
- console.error("Error fetching items:", err);
- }
- setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
- }
- }
- };
-
- return (
-
-
- {brands.length === 0 && (
-
-
- No brands selected yet.
-
-
- )}
-
- {brands.map((brand) => (
-
-
-
-
-
- ID: {brand.id}
-
-
-
-
-
- {expandedBrand === brand.id && (
-
-
- {actionData?.success && (
-
-
- {actionData.results.map((r) => (
-
- Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
-
- ))}
-
-
- )}
-
-
-
-
- {loadingMap[brand.id] ? (
-
- ) : (
-
- {(itemsMap[brand.id] || []).map((item) => (
-
-
-
-
-
-
-
- Part Number: {item.attributes.part_number}
- Category: {item.attributes.category} > {item.attributes.subcategory}
-
-
-
-
- ))}
-
- )}
-
-
- )}
-
- ))}
-
-
- );
-}
diff --git a/app/routes/app.managebrand_bak.jsx b/app/routes/app.managebrand_bak.jsx
new file mode 100644
index 0000000..1e6954a
--- /dev/null
+++ b/app/routes/app.managebrand_bak.jsx
@@ -0,0 +1,297 @@
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ Card,
+ Thumbnail,
+ TextContainer,
+ Spinner,
+ Button,
+ Text,
+ TextField,
+} from "@shopify/polaris";
+import { useState } from "react";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }) => {
+ const { admin } = await authenticate.admin(request);
+ const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
+ const accessToken = await getTurn14AccessTokenFromMetafield(request);
+
+ const res = await admin.graphql(`
+ {
+ shop {
+ metafield(namespace: "turn14", key: "selected_brands") {
+ value
+ }
+ }
+ }
+ `);
+
+ const data = await res.json();
+ const rawValue = data?.data?.shop?.metafield?.value;
+
+ let brands = [];
+ try {
+ brands = JSON.parse(rawValue);
+ } catch (err) {
+ console.error("❌ Failed to parse metafield value:", err);
+ }
+
+ return json({ brands, accessToken });
+};
+
+export default function ManageBrandProducts() {
+ const { brands, accessToken } = useLoaderData();
+ const [expandedBrand, setExpandedBrand] = useState(null);
+ const [itemsMap, setItemsMap] = useState({});
+ const [loadingMap, setLoadingMap] = useState({});
+ const [productCount, setProductCount] = useState("10");
+ const [adding, setAdding] = useState(false);
+
+ const toggleBrandItems = async (brandId) => {
+ const isExpanded = expandedBrand === brandId;
+ if (isExpanded) {
+ setExpandedBrand(null);
+ } else {
+ setExpandedBrand(brandId);
+ if (!itemsMap[brandId]) {
+ setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
+ try {
+ const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ const data = await res.json();
+ setItemsMap((prev) => ({ ...prev, [brandId]: data }));
+ } catch (err) {
+ console.error("Error fetching items:", err);
+ }
+ setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
+ }
+ }
+ };
+
+ const handleAddProducts = async (brandId) => {
+ const count = parseInt(productCount || "10");
+ const items = (itemsMap[brandId] || []).slice(0, count);
+ if (!items.length) return alert("No products to add.");
+ setAdding(true);
+
+ for (const item of items) {
+ const attr = item.attributes;
+
+ // Step 1: Create Product (only allowed fields)
+ const productInput = {
+ title: attr.product_name,
+ descriptionHtml: `
${attr.part_description}
`,
+ vendor: attr.brand,
+ productType: attr.category,
+ tags: [attr.subcategory, attr.brand].filter(Boolean).join(", "),
+ };
+
+ const createProductRes = await fetch("/admin/api/2023-04/graphql.json", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Shopify-Access-Token": accessToken,
+ },
+ body: JSON.stringify({
+ query: `
+ mutation productCreate($input: ProductInput!) {
+ productCreate(input: $input) {
+ product {
+ id
+ title
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }`,
+ variables: { input: productInput },
+ }),
+ });
+
+ const createProductResult = await createProductRes.json();
+ const product = createProductResult?.data?.productCreate?.product;
+ const productErrors = createProductResult?.data?.productCreate?.userErrors;
+
+ if (productErrors?.length || !product?.id) {
+ console.error("❌ Product create error:", productErrors);
+ continue;
+ }
+
+ const productId = product.id;
+
+ // Step 2: Create Variant
+ const variantInput = {
+ productId,
+ sku: attr.part_number,
+ barcode: attr.barcode || undefined,
+ price: "0.00",
+ weight: attr.dimensions?.[0]?.weight || 0,
+ weightUnit: "KILOGRAMS",
+ inventoryManagement: "SHOPIFY",
+ };
+
+ await fetch("/admin/api/2023-04/graphql.json", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Shopify-Access-Token": accessToken,
+ },
+ body: JSON.stringify({
+ query: `
+ mutation productVariantCreate($input: ProductVariantInput!) {
+ productVariantCreate(input: $input) {
+ productVariant {
+ id
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }`,
+ variables: { input: variantInput },
+ }),
+ });
+
+ // Step 3: Add Image
+ if (attr.thumbnail) {
+ await fetch("/admin/api/2023-04/graphql.json", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Shopify-Access-Token": accessToken,
+ },
+ body: JSON.stringify({
+ query: `
+ mutation productImageCreate($productId: ID!, $image: ImageInput!) {
+ productImageCreate(productId: $productId, image: $image) {
+ image {
+ id
+ src
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }`,
+ variables: {
+ productId,
+ image: {
+ src: attr.thumbnail,
+ },
+ },
+ }),
+ });
+ }
+
+ console.log("✅ Added:", attr.product_name);
+ }
+
+ setAdding(false);
+ alert(`${items.length} products added.`);
+ };
+
+ return (
+
+
+ {brands.length === 0 && (
+
+
+ No brands selected yet.
+
+
+ )}
+
+ {brands.map((brand) => (
+
+
+
+
+
+ Brand: {brand.name}
+ ID: {brand.id}
+
+
+
+
+
+
+
+ {expandedBrand === brand.id && (
+
+
+
+
+
+
+
+
+
+ {loadingMap[brand.id] ? (
+
+ ) : (
+
+ {(itemsMap[brand.id] || []).map(item => (
+
+
+
+
+
+
+
+ Part Number: {item.attributes.part_number}
+ Brand: {item.attributes.brand}
+ Category: {item.attributes.category} > {item.attributes.subcategory}
+ Dimensions: {item.attributes.dimensions?.[0]?.length} x {item.attributes.dimensions?.[0]?.width} x {item.attributes.dimensions?.[0]?.height} in
+
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/app/routes/app.settings.jsx b/app/routes/app.settings.jsx
index f9948a9..6f8924e 100644
--- a/app/routes/app.settings.jsx
+++ b/app/routes/app.settings.jsx
@@ -1,4 +1,4 @@
-import { json } from "@remix-run/node";
+ import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
import { useState } from "react";
import {
@@ -10,6 +10,7 @@ import {
TextContainer,
InlineError,
} from "@shopify/polaris";
+import { TitleBar } from "@shopify/app-bridge-react";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
@@ -136,7 +137,7 @@ export const action = async ({ request }) => {
}
};
-export default function SettingsPage() {
+export default function SettingsPage({ standalone = true }) {
const loaderData = useLoaderData();
const actionData = useActionData();
@@ -148,7 +149,8 @@ export default function SettingsPage() {
const displayToken = actionData?.accessToken || savedCreds.accessToken;
return (
-
+
+
@@ -206,3 +208,220 @@ export default function SettingsPage() {
);
}
+
+/*
+import { json } from "@remix-run/node";
+import { useLoaderData, useActionData, Form } from "@remix-run/react";
+import { useState } from "react";
+import {
+ Page,
+ Layout,
+ Card,
+ TextField,
+ Button,
+ InlineError,
+ BlockStack,
+ Text,
+} from "@shopify/polaris";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }) => {
+ const { admin } = await authenticate.admin(request);
+
+ const gqlResponse = await admin.graphql(`
+ {
+ shop {
+ id
+ name
+ metafield(namespace: "turn14", key: "credentials") {
+ value
+ }
+ }
+ }
+ `);
+
+ const shopData = await gqlResponse.json();
+ const shopName = shopData?.data?.shop?.name || "Unknown Shop";
+ const metafieldRaw = shopData?.data?.shop?.metafield?.value;
+
+ let creds = {};
+ if (metafieldRaw) {
+ try {
+ creds = JSON.parse(metafieldRaw);
+ } catch (err) {
+ console.error("Failed to parse stored credentials:", err);
+ }
+ }
+
+ return json({ shopName, creds });
+};
+
+export const action = async ({ request }) => {
+ const formData = await request.formData();
+ const clientId = formData.get("client_id") || "";
+ const clientSecret = formData.get("client_secret") || "";
+
+ const { admin } = await authenticate.admin(request);
+
+ const shopInfo = await admin.graphql(`{ shop { id } }`);
+ const shopId = (await shopInfo.json())?.data?.shop?.id;
+
+ 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,
+ }),
+ });
+
+ const tokenData = await tokenRes.json();
+
+ if (!tokenRes.ok) {
+ return json({
+ success: false,
+ error: tokenData.error || "Failed to fetch access token",
+ });
+ }
+
+ const accessToken = tokenData.access_token;
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
+
+ const credentials = {
+ clientId,
+ clientSecret,
+ accessToken,
+ expiresAt,
+ };
+
+ const mutation = `
+ mutation {
+ metafieldsSet(metafields: [
+ {
+ ownerId: "${shopId}"
+ namespace: "turn14"
+ key: "credentials"
+ type: "json"
+ value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}"
+ }
+ ]) {
+ metafields {
+ key
+ value
+ }
+ userErrors {
+ field
+ message
+ }
+ }
+ }
+ `;
+
+ const saveRes = await admin.graphql(mutation);
+ const result = await saveRes.json();
+
+ if (result?.data?.metafieldsSet?.userErrors?.length) {
+ return json({
+ success: false,
+ error: result.data.metafieldsSet.userErrors[0].message,
+ });
+ }
+
+ return json({
+ success: true,
+ clientId,
+ clientSecret,
+ accessToken,
+ });
+ } catch (err) {
+ console.error("Turn14 token fetch failed:", err);
+ return json({
+ success: false,
+ error: "Network or unexpected error occurred",
+ });
+ }
+};
+
+export default function SettingsPage({ standalone = true }) {
+ const loaderData = useLoaderData();
+ const actionData = useActionData();
+
+ const savedCreds = loaderData?.creds || {};
+ const shopName = loaderData?.shopName || "Shop";
+
+ const [clientId, setClientId] = useState(
+ actionData?.clientId || savedCreds.clientId || ""
+ );
+ const [clientSecret, setClientSecret] = useState(
+ actionData?.clientSecret || savedCreds.clientSecret || ""
+ );
+ const displayToken = actionData?.accessToken || savedCreds.accessToken;
+
+ const content = (
+
+
+
+
+ Connected Shop: {shopName}
+
+
+
+ {actionData?.error && (
+
+ )}
+
+ {displayToken && (
+
+
+ ✅ Access token:
+
+
+ {displayToken}
+
+
+ )}
+
+
+
+
+ );
+
+ return standalone ? {content} : content;
+}
+ */
\ No newline at end of file
diff --git a/app/shopify.server.js b/app/shopify.server.js
index a11bf9d..c73d8f6 100644
--- a/app/shopify.server.js
+++ b/app/shopify.server.js
@@ -7,6 +7,9 @@ import {
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
+import dotenv from 'dotenv';
+dotenv.config();
+
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
diff --git a/package-lock.json b/package-lock.json
index eb7d5bd..9fb90fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@shopify/polaris": "^12.27.0",
"@shopify/shopify-app-remix": "^3.7.0",
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
+ "dotenv": "^17.0.0",
"isbot": "^5.1.0",
"prisma": "^6.2.1",
"react": "^18.2.0",
@@ -2034,6 +2035,19 @@
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
+ "node_modules/@graphql-tools/prisma-loader/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/@graphql-tools/relay-operation-optimizer": {
"version": "7.0.19",
"resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.19.tgz",
@@ -2958,6 +2972,18 @@
}
}
},
+ "node_modules/@remix-run/dev/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/@remix-run/dev/node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
@@ -6462,9 +6488,10 @@
}
},
"node_modules/dotenv": {
- "version": "16.5.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
- "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.0.tgz",
+ "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
diff --git a/package.json b/package.json
index adc02f9..a639117 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@shopify/polaris": "^12.27.0",
"@shopify/shopify-app-remix": "^3.7.0",
"@shopify/shopify-app-session-storage-prisma": "^6.0.0",
+ "dotenv": "^17.0.0",
"isbot": "^5.1.0",
"prisma": "^6.2.1",
"react": "^18.2.0",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index af4a01d..37cc464 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1,20 +1,14 @@
-// This is your Prisma schema file,
-// learn more about it in the docs: https://pris.ly/d/prisma-schema
-
generator client {
provider = "prisma-client-js"
}
-// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
-// enough when changing adapters.
-// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
datasource db {
provider = "sqlite"
url = "file:dev.sqlite"
}
model Session {
- id String @id
+ id String @id
shop String
state String
isOnline Boolean @default(false)
@@ -32,12 +26,12 @@ model Session {
}
model Turn14Credential {
- id String @id @default(cuid())
- shop String @unique
- clientId String
- clientSecret String
- accessToken String
- expiresAt DateTime
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @default(cuid())
+ shop String @unique
+ clientId String
+ clientSecret String
+ accessToken String
+ expiresAt DateTime
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
}
diff --git a/shopify.app.toml b/shopify.app.toml
index 142e22b..5761663 100644
--- a/shopify.app.toml
+++ b/shopify.app.toml
@@ -1,9 +1,7 @@
-# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
-
client_id = "b7534c980967bad619cfdb9d3f837cfa"
name = "turn14-test"
handle = "turn14-test-1"
-application_url = "https://manhattan-fifty-pays-detector.trycloudflare.com"
+application_url = "https://shopify.data4autos.com" # Update this line
embedded = true
[build]
@@ -22,11 +20,7 @@ api_version = "2025-04"
uri = "/webhooks/app/uninstalled"
[access_scopes]
-# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_inventory,read_products,write_inventory,write_products"
[auth]
-redirect_urls = ["https://manhattan-fifty-pays-detector.trycloudflare.com/auth/callback", "https://manhattan-fifty-pays-detector.trycloudflare.com/auth/shopify/callback", "https://manhattan-fifty-pays-detector.trycloudflare.com/api/auth/callback"]
-
-[pos]
-embedded = false
+redirect_urls = ["https://shopify.data4autos.com/auth/callback", "https://shopify.data4autos.com/auth/shopify"] # Update this line as well