Replit prompt — “Icons with Attribution + Pro unlock” (fits your current page)

Goal: Add server-side licensing/attribution for imported icons only while keeping the current Icons page behavior unchanged for mock icons.

For imported icons: add entitlement checks + backend download (raw if licensed/Pro, ZIP with CREDIT.txt if free).

For mock/demo icons: keep current client-side copy/download exactly as is.

Backend

Create routes/iconLibraryRoutes.js with:

GET /api/icons/list → { icons: [...] } (id, name, style, tags, svgKey, pngKey, previewKey, createdAt).

GET /api/icons/:id/entitlement → { licensed, requiresAttribution, canDownload:true } using services/entitlements.js and a helper userHasPro(userId) (stub false if no auth yet).

GET /api/icons/:id/download?format=svg|png

If licensed (hasLicense or userHasPro) → stream raw file from icons/svg/{id}/... or icons/png/{id}/....

Else → return a ZIP (icon file + CREDIT.txt with the provided copy).

(Optional) POST /api/icons/upload if you need an admin pipeline.

Storage keys to use:

SVG → icons/svg/{id}/{filename}.svg

PNG → icons/png/{id}/{filename}.png

Preview → icons/preview/{id}/{filename}.png

In server.js, wire routes without changing anything else:

const iconLibraryRoutes = require("./routes/iconLibraryRoutes");
// keep webhook BEFORE json
app.use("/api/stripe", stripeWebhook);
app.use(express.json());
app.use("/api/icons", iconLibraryRoutes);
// Compatibility alias for your page:
app.get("/api/icons/imported", async (req, res) => {
  req.url = "/list"; // same payload shape: { icons: [...] }
  iconLibraryRoutes.handle(req, res);
});


Add services/iconsAttribution.js:

// services/iconsAttribution.js
function buildAttribution({ iconName, author = "IBrandBiz Icons" }) {
  const text = `Icon "${iconName}" by ${author} — Free with Attribution (https://ibrandbiz.com/icons)`;
  const html = `Icon “${iconName}” by <a href="https://ibrandbiz.com/icons" target="_blank" rel="noopener">IBrandBiz Icons</a> — Free with Attribution.`;
  return { text, html };
}
module.exports = { buildAttribution };


CREDIT.txt content for ZIP (free users):

Thank you for using IBrandBiz Icons!

License: Free with Attribution
You MAY use these icons commercially, modify them, and publish in unlimited projects.
You MUST include a visible credit where the icons appear (site footer, About/Credits page, caption, or end credits):

"Icons by IBrandBiz Icons — Free with Attribution (https://ibrandbiz.com/icons)"

Full license: https://ibrandbiz.com/license/icons/free
Upgrading to IBrandBiz Pro removes the attribution requirement: https://ibrandbiz.com/pricing

Frontend (additive, non-breaking)

Add AttributionNotice and a small entitlement hook:

src/components/AttributionNotice.tsx (as given earlier).

src/hooks/useIconEntitlement.ts (as given earlier).

In your Icons page, only switch the Download button to use the backend when the icon is imported.

Keep current Copy SVG and client-side Download for mock icons.

For imported icons (icon.id starts with imported-):

call useIconEntitlement(rawId) where rawId = icon.id.replace(/^imported-/, "").

If licensed → show Download SVG/PNG links to /api/icons/:rawId/download?...

Else → show Download Free (Attribution) → /api/icons/:rawId/download?format=svg and render <AttributionNotice/>.

✅ No other UI or behavior changes.

1–5 minimal changes (with tiny code you can paste)
1) Backend routes (add) — routes/iconLibraryRoutes.js

(Skeleton only; storage integration mirrors your object storage helpers from photos/mockups)

const express = require("express");
const archiver = require("archiver");
const ObjectStorageService = require("../services/ObjectStorageService");
const { hasLicense } = require("../services/entitlements");
const { buildAttribution } = require("../services/iconsAttribution");

const router = express.Router();

// TODO: replace with real list from your store; ensure { icons: [...] } matches your FE
router.get("/list", async (_req, res) => {
  // Example: list previews under icons/preview/
  const files = await ObjectStorageService.listPrefix("icons/preview/");
  const items = files.map(f => {
    // icons/preview/{id}/{filename}.png
    const parts = f.key.split("/");
    const id = parts[2];
    return {
      id,                       // raw id (your FE will prepend 'imported-' when it combines)
      name: parts[3].replace(/\.(png|svg)$/i, ""),
      style: "outlined",
      tags: [],
      previewUrl: ObjectStorageService.publicUrlFromKey(f.key),
      formats: ["svg","png"],
      createdAt: Date.now()
    };
  });
  res.json({ icons: items });
});

function userHasPro(userId){ return false; } // TODO: wire to your auth/subs later

router.get("/:id/entitlement", async (req, res) => {
  const userId = req.user?.id || null;
  const id = req.params.id;
  const licensed = userId ? (hasLicense(userId, id) || userHasPro(userId)) : false;
  res.json({ licensed, requiresAttribution: !licensed, canDownload: true });
});

router.get("/:id/download", async (req, res) => {
  const id = req.params.id;
  const fmt = (req.query.format || "svg").toString().toLowerCase(); // svg|png

  // Resolve storage key
  const base = fmt === "png" ? "icons/png" : "icons/svg";
  const [file] = await ObjectStorageService.listPrefix(`${base}/${id}/`);
  if (!file) return res.status(404).send("Not found");

  const userId = req.user?.id || null;
  const licensed = userId ? (hasLicense(userId, id) || userHasPro(userId)) : false;

  if (licensed) {
    const stream = await ObjectStorageService.getStream(file.key);
    res.setHeader("Content-Type", fmt === "png" ? "image/png" : "image/svg+xml");
    res.setHeader("Content-Disposition", `attachment; filename="IBrandBiz_${id}.${fmt}"`);
    return stream.pipe(res);
  }

  // Free with attribution → build a zip {icon + CREDIT.txt}
  const archive = archiver("zip", { zlib: { level: 9 } });
  res.setHeader("Content-Type", "application/zip");
  res.setHeader("Content-Disposition", `attachment; filename="IBrandBiz_${id}_${fmt}_FREE.zip"`);

  const credit = [
    "Thank you for using IBrandBiz Icons!",
    "",
    "License: Free with Attribution",
    'You MUST include visible credit: "Icons by IBrandBiz Icons — Free with Attribution (https://ibrandbiz.com/icons)"',
    "Full license: https://ibrandbiz.com/license/icons/free",
    "Upgrade to IBrandBiz Pro to remove attribution: https://ibrandbiz.com/pricing",
    ""
  ].join("\n");

  archive.append(credit, { name: "CREDIT.txt" });
  const fileStream = await ObjectStorageService.getStream(file.key);
  archive.append(fileStream, { name: `icon.${fmt}` });
  archive.finalize();
  archive.pipe(res);
});

module.exports = router;

2) server.js wiring (additive)
const iconLibraryRoutes = require("./routes/iconLibraryRoutes");

// keep webhook before json
app.use("/api/stripe", stripeWebhook);
app.use(express.json());
app.use("/api/icons", iconLibraryRoutes);

// FE compatibility for your existing fetch('/api/icons/imported')
app.get("/api/icons/imported", (req, res) => {
  req.url = "/list";
  iconLibraryRoutes.handle(req, res);
});

3) Add front-end helpers (new files)

src/components/AttributionNotice.tsx (same as earlier)

src/hooks/useIconEntitlement.ts (same as earlier)

4) Tiny, non-breaking tweak in your Icons grid

Only modify the Download button for imported icons. Keep mock icons unchanged.

Find this block in your grid card actions (first “Download” button):

<button
  className="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted"
  onClick={() => {
    try {
      const safeColored = sanitizeSvgClient(applySvgColor(icon.svg, color));
      downloadTextFile(`${icon.name}.svg`, safeColored);
    } catch (error) {
      console.error('SVG sanitization failed:', error);
    }
  }}
  data-testid={`button-download-${icon.id}`}
>
  <Download className="w-4 h-4" /> Download
</button>


Replace with this conditional block:

{icon.id.startsWith("imported-") ? (
  // Use backend licensing for imported icons
  (() => {
    const rawId = icon.id.replace(/^imported-/, "");
    // lightweight inline hook usage:
    // (you can move this to a custom component if preferred)
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const ent = require("../hooks/useIconEntitlement").useIconEntitlement(rawId);

    return ent.loading ? (
      <button className="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg border border-border bg-gray-100">
        Checking…
      </button>
    ) : ent.licensed ? (
      <a
        href={`/api/icons/${rawId}/download?format=svg`}
        className="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted"
        data-testid={`button-download-${icon.id}`}
      >
        <Download className="w-4 h-4" /> Download SVG
      </a>
    ) : (
      <>
        <a
          href={`/api/icons/${rawId}/download?format=svg`}
          className="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted"
          data-testid={`button-download-${icon.id}`}
        >
          <Download className="w-4 h-4" /> Download Free (Attribution)
        </a>
      </>
    );
  })()
) : (
  // Keep existing client-side download for mock/demo icons
  <button
    className="inline-flex items-center justify-center gap-2 px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted"
    onClick={() => {
      try {
        const safeColored = sanitizeSvgClient(applySvgColor(icon.svg, color));
        downloadTextFile(`${icon.name}.svg`, safeColored);
      } catch (error) {
        console.error('SVG sanitization failed:', error);
      }
    }}
    data-testid={`button-download-${icon.id}`}
  >
    <Download className="w-4 h-4" /> Download
  </button>
)}


Optionally, below that action row, add the attribution notice only for imported + not licensed:

{icon.id.startsWith("imported-") && !require("../hooks/useIconEntitlement").useIconEntitlement(icon.id.replace(/^imported-/, "")).licensed && (
  <div className="mt-2">
    {(() => {
      const { buildAttribution } = require("../utils/iconAttribution");
      const AttributionNotice = require("../components/AttributionNotice").default;
      const attrs = buildAttribution(icon.name);
      return <AttributionNotice {...attrs} />;
    })()}
  </div>
)}


(If you prefer, move that to a small child component to avoid inline requires.)

5) Quick acceptance test

Your page still shows mock icons with Copy SVG and client-side Download unchanged.

When your backend lists imported icons at /api/icons/imported, those cards now:

Call /api/icons/:id/entitlement.

Show Download SVG (no attribution) if licensed/Pro.

Show Download Free (Attribution) + notice if not licensed (ZIP delivered).