fix: use client-side navigation for nav cards and auto-refresh expired Turn14 tokens

- NavCard now uses useNavigate() instead of <a href> to prevent full-page
  reloads that trigger Shopify re-auth (login page prompt)
- Browse Brands and Manage Brands links changed from absolute admin.shopify.com
  URLs (caused iframe 'refused to connect') to relative /app/* paths
- turn14Token.server.js: add forceRefresh param to skip cached token check
- app.brands.jsx: detect 401/internal_error from Turn14 and retry once with
  a forced token refresh to recover from server-side token expiry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MOHAN 2026-07-02 23:19:05 +05:30
parent d165883d9c
commit d54bda3179
3 changed files with 24 additions and 15 deletions

View File

@ -138,6 +138,7 @@ export const action = async ({ request }) => {
/* ─── NavCard ────────────────────────────────────────────────────────────────── */
function NavCard({ icon, title, desc, link, accent }) {
const navigate = useNavigate();
const colors = {
blue: { bg: "#eff6ff", border: "#bfdbfe", icon: "#2563eb" },
green: { bg: "#f0fdf4", border: "#bbf7d0", icon: "#16a34a" },
@ -146,8 +147,9 @@ function NavCard({ icon, title, desc, link, accent }) {
};
const c = colors[accent] || colors.blue;
return (
<a href={link} style={{ textDecoration: "none", display: "block" }}>
<div style={{ background: c.bg, border: `1px solid ${c.border}`, borderRadius: 12, padding: "18px 20px", cursor: "pointer", transition: "transform 0.15s, box-shadow 0.15s", height: "100%", display: "flex", flexDirection: "column", gap: 10 }}
<div
onClick={() => navigate(link)}
style={{ background: c.bg, border: `1px solid ${c.border}`, borderRadius: 12, padding: "18px 20px", cursor: "pointer", transition: "transform 0.15s, box-shadow 0.15s", height: "100%", display: "flex", flexDirection: "column", gap: 10 }}
onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.08)"; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = ""; }}
>
@ -155,7 +157,6 @@ function NavCard({ icon, title, desc, link, accent }) {
<div style={{ fontWeight: 700, fontSize: 15, color: c.icon }}>{title}</div>
<div style={{ fontSize: 13, color: "#6b7280", lineHeight: 1.5 }}>{desc}</div>
</div>
</a>
);
}
@ -203,8 +204,8 @@ export default function Index() {
const navItems = [
{ icon: "⚙️", title: "Settings", desc: "Connect Turn14 API & configure pricing", link: `/app/settings`, accent: "purple" },
{ icon: "🏷️", title: "Browse Brands", desc: "Select brands to sync from Turn14", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands`, accent: "blue" },
{ icon: "📦", title: "Manage Brands", desc: "Import products from selected brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand`, accent: "green" },
{ icon: "🏷️", title: "Browse Brands", desc: "Select brands to sync from Turn14", link: `/app/brands`, accent: "blue" },
{ icon: "📦", title: "Manage Brands", desc: "Import products from selected brands", link: `/app/managebrand`, accent: "green" },
{ icon: "📊", title: "Dashboard", desc: "Track live import progress & stats", link: `/app/dashboard`, accent: "amber" },
];

View File

@ -120,8 +120,16 @@ export const loader = async ({ request }) => {
let brandJson;
try {
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } });
let brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } });
brandJson = await brandRes.json();
// Token expired on Turn14's side force refresh and retry once
if (!brandRes.ok && (brandRes.status === 401 || brandJson?.error === "internal_error" || brandJson?.error === "invalid_token")) {
try {
accessToken = await getTurn14AccessTokenFromMetafield(request, true);
brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } });
brandJson = await brandRes.json();
} catch {}
}
if (!brandRes.ok) return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, error: brandJson?.error || "Failed to fetch brands", isSubscribed, subscription });
} catch (err) {
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, error: "Turn14 brands fetch crashed", isSubscribed, subscription });

View File

@ -1,6 +1,6 @@
import { authenticate } from "../shopify.server";
export async function getTurn14AccessTokenFromMetafield(request) {
export async function getTurn14AccessTokenFromMetafield(request, forceRefresh = false) {
const { admin, session } = await authenticate.admin(request);
const shop = session.shop;
@ -35,7 +35,7 @@ export async function getTurn14AccessTokenFromMetafield(request) {
const expiresAt = new Date(creds.expiresAt);
const isExpired = now > expiresAt;
if (!isExpired && creds.accessToken) {
if (!forceRefresh && !isExpired && creds.accessToken) {
return creds.accessToken;
}