diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31582ac4f..8a9f2aebd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,3 +103,6 @@ jobs: - name: 🧪 Frontend tests (coverage-gated) run: cd apps/frontend && bun run test:coverage + + - name: 🧪 Gold tests + run: bun run test:gold diff --git a/apps/frontend/netlify.toml b/apps/frontend/netlify.toml index fd989cce8..0a4fd7319 100644 --- a/apps/frontend/netlify.toml +++ b/apps/frontend/netlify.toml @@ -25,3 +25,15 @@ # apps/gold or the shared/sdk packages would be skipped as "no content change". Build when # any input of this site changed (paths are relative to the base directory). ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- . ../gold ../../packages/shared ../../packages/sdk" + +# Gold signs Ethereum transactions and collects PIX keys and KYC data, so it must not be framed +# by other sites (clickjacking) and keeps the baseline headers of its standalone release +# (gold.satoshipay.io). Scoped to Gold so the rest of the site, e.g. the widget, is unchanged. +[[headers]] + for = "/pt-br/gold/*" + [headers.values] + Content-Security-Policy = "frame-ancestors 'none'" + X-Frame-Options = "DENY" + X-Content-Type-Options = "nosniff" + Referrer-Policy = "strict-origin-when-cross-origin" + Permissions-Policy = "camera=(self), microphone=(), geolocation=()" diff --git a/apps/frontend/src/tests/netlify-headers.test.ts b/apps/frontend/src/tests/netlify-headers.test.ts new file mode 100644 index 000000000..99400d536 --- /dev/null +++ b/apps/frontend/src/tests/netlify-headers.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +// Reads the [[headers]] blocks of netlify.toml: a `for = ""` line followed by `Name = "value"` lines. +const headerRules = readFileSync(new URL("../../netlify.toml", import.meta.url), "utf8") + .split("[[headers]]") + .slice(1) + .map(block => ({ + path: block.match(/^\s*for\s*=\s*"([^"]+)"/m)?.[1], + values: Object.fromEntries( + [...block.matchAll(/^\s*([A-Za-z-]+)\s*=\s*"([^"]*)"\s*$/gm)] + .filter(([, name]) => name !== "for") + .map(([, name, value]) => [name, value]) + ) + })); + +describe("Netlify headers", () => { + it("keeps the gold app out of third-party frames", () => { + expect(headerRules.find(rule => rule.path === "/pt-br/gold/*")?.values).toMatchObject({ + "Content-Security-Policy": "frame-ancestors 'none'", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY" + }); + }); + + it("does not apply the anti-framing headers beyond gold", () => { + expect(headerRules.filter(rule => rule.values["X-Frame-Options"]).map(rule => rule.path)).toEqual(["/pt-br/gold/*"]); + }); +}); diff --git a/apps/gold/public/assets/gold-bar-cutout.png b/apps/gold/public/assets/gold-bar-cutout.png deleted file mode 100644 index bf18eefeb..000000000 Binary files a/apps/gold/public/assets/gold-bar-cutout.png and /dev/null differ diff --git a/apps/gold/public/assets/gold-bar-cutout.webp b/apps/gold/public/assets/gold-bar-cutout.webp new file mode 100644 index 000000000..01ac87d03 Binary files /dev/null and b/apps/gold/public/assets/gold-bar-cutout.webp differ diff --git a/apps/gold/public/brand/ouro-wordmark.png b/apps/gold/public/brand/ouro-wordmark.png index cee7b209f..0024a35f7 100644 Binary files a/apps/gold/public/brand/ouro-wordmark.png and b/apps/gold/public/brand/ouro-wordmark.png differ diff --git a/apps/gold/src/App.jsx b/apps/gold/src/App.jsx index ea1a65f17..1d79f2607 100644 --- a/apps/gold/src/App.jsx +++ b/apps/gold/src/App.jsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ArrowLeft, ArrowRight, Bank, CaretDown, Check, CheckCircle, Clock, Copy, EnvelopeSimple, Eye, EyeSlash, Fingerprint, Info, LockKey, SealCheck, ShieldCheck, SignOut, TrendUp, Wallet, WarningCircle, X } from "@phosphor-icons/react"; +import { ArrowLeft, ArrowRight, Bank, CaretDown, Check, CheckCircle, Clock, Copy, EnvelopeSimple, Eye, EyeSlash, Fingerprint, Info, LockKey, SealCheck, ShieldCheck, SignOut, TrendDown, TrendUp, Wallet, WarningCircle, X } from "@phosphor-icons/react"; import { SellFlow } from "./SellFlow.jsx"; import { MIN_BUY, QUICK_BUY_VALUES, DEFAULT_BUY, validBuyAmount, buyFeePercent } from "./lib/purchase-options.js"; import { QRCodeSVG } from "qrcode.react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis } from "recharts"; -import { fetchPaxgMarket, getDemoMarket } from "./lib/market.js"; +import { CHART_PERIODS, chartWindow, fetchPaxgMarket, getDemoMarket } from "./lib/market.js"; import { readPaxgBalance } from "./lib/paxg.js"; -import { addRampHistory, clearActiveRamp, getActiveRamp, getRampHistory, saveActiveRamp } from "./lib/pilot-store.js"; +import { addRampHistory, clearActiveRamp, failActiveRamp, getActiveRamp, getRampHistory, saveActiveRamp } from "./lib/pilot-store.js"; import { classifyRamp, clearVortexSession, @@ -16,10 +16,13 @@ import { getBrazilBuyReadiness, getPaxgAvailability, getBrazilKycUploads, + hasVortexSession, pollBrazilKyc, pollRamp, + rampStartDeadline, registerPaxgBuy, requestVortexOtp, + secondsUntilExpiry, startRampSafely, submitBrazilKyc, uploadKycDocument, @@ -55,16 +58,16 @@ function Modal({ title, description, onClose, children, wide = false, closeDisab } function Landing({ onStart, onLearn }) { - return

COMPRE OURO COM PIX

Comprar ouro
ficou simples.

Escolha um valor. Pague com PIX.
{" "}Receba ouro com lastro físico, direto na sua carteira.

Barra de ouro fino de um quilograma
12,6 g

seu saldo em ouro

A partir de {formatBRL(MIN_BUY, 0)}