Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,37 @@ jobs:

# The cache holds browsers, not the apt packages they link against,
# so system deps still need installing on a cache hit.
#
# `apt-get update` inside install-deps stalls intermittently on the
# runner's Azure mirrors: three consecutive 6-minute timeouts on
# 2026-08-19 (PR #2755), all of them ignoring
# azure.archive.ubuntu.com and then hanging on noble-security, while
# sibling runs minutes apart cleared the same step in seconds. The
# stall is transient, so retry with a per-attempt cap — one bad
# mirror then costs an attempt instead of the whole job — and drop
# to the canonical archive after the first failure. `timeout` owns
# the attempt's process group, so --kill-after reaps an apt child
# that ignores the SIGTERM it sends first. Still exits
# non-zero once the attempts are spent: a genuinely missing system
# library must stay a red job, not a silent one.
- name: Install Playwright system deps
if: steps.pw-cache.outputs.cache-hit == 'true'
timeout-minutes: 6
run: npx playwright install-deps chromium
timeout-minutes: 9
run: |
for attempt in 1 2 3; do
if timeout --kill-after=15s 150 npx playwright install-deps chromium; then
exit 0
fi
echo "::warning::playwright install-deps attempt ${attempt} failed or stalled; retrying"
sudo rm -rf /var/lib/apt/lists/partial/* || true
if [ "${attempt}" = 1 ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' \
/etc/apt/sources.list.d/*.sources /etc/apt/sources.list || true
fi
sleep 5
done
echo "::error::playwright install-deps failed after 3 attempts"
exit 1

- name: Run E2E tests
run: pnpm test:e2e
Expand Down
8 changes: 2 additions & 6 deletions src/components/LandingPage/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,8 @@ const Footer = ({
<Link className={NAV_LINK} href={`/${locale}/help`}>
{i18n.footerDocs}
</Link>
<Link className={NAV_LINK} href={`/${locale}/terms`}>
{i18n.footerTerms}
</Link>
<Link className={NAV_LINK} href={`/${locale}/privacy`}>
{i18n.footerPrivacy}
</Link>
{/* Terms and Privacy deliberately live in the Legal column of
the site directory below, not here — one home per document. */}
<Link className={NAV_LINK} href={`/${locale}/help/security-disclosure`}>
{i18n.footerSecurity}
</Link>
Expand Down
63 changes: 52 additions & 11 deletions src/components/LandingPage/SEOFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Link from 'next/link'
import manifest from '@/content/generated/footer-manifest.json'
import { getTranslations, t } from '@/i18n'
import { DEFAULT_LOCALE, type Locale } from '@/i18n/types'
import { DEFAULT_LOCALE, type Locale, type Translations } from '@/i18n/types'

// SEO footer driven by the content manifest (peanut-content/generated/footer-manifest.json).
// Data is imported as a JSON module — works in both client and server components
Expand All @@ -28,6 +28,41 @@ function FooterSection({ title, children }: { title: string; children: React.Rea
)
}

/**
* Manifest "resources" entries that now live elsewhere in the footer, so the
* remainder can be folded into "Learn More" without duplicating a link:
* - terms → a stale Notion export, superseded by the /terms page in Legal below
* - jobs → already a top-level link in the footer nav above
*/
const RESOURCES_MOVED_ELSEWHERE = new Set(['terms', 'jobs'])

/**
* Every published legal document, in the order a reader needs them: the two
* that bind all users first, then the card-programme docs. Card applicants see
* these inline at signing time (CardTermsScreen), but app-store review and the
* issuer both expect them permanently reachable, which is what this column is.
* Hrefs are authored `/en/…` and localized by `localizeHref` like the manifest
* entries; the marketing pages fall back to English prose when a translation
* is missing, so a localized URL is always safe.
*/
const LEGAL_LINKS: Array<{ slug: string; href: string; label: (i18n: Translations) => string }> = [
{ slug: 'terms', href: '/en/terms', label: (i18n) => i18n.footerTerms },
{ slug: 'privacy', href: '/en/privacy', label: (i18n) => i18n.footerPrivacy },
{ slug: 'card-terms-us', href: '/en/card-terms-us', label: (i18n) => i18n.footerCardTermsUs },
{
slug: 'card-terms-international',
href: '/en/card-terms-international',
label: (i18n) => i18n.footerCardTermsInternational,
},
{ slug: 'card-esign', href: '/en/card-esign', label: (i18n) => i18n.footerCardEsign },
{ slug: 'card-privacy', href: '/en/card-privacy', label: (i18n) => i18n.footerCardPrivacy },
{
slug: 'card-prohibited-activities',
href: '/en/card-prohibited-activities',
label: (i18n) => i18n.footerCardProhibitedActivities,
},
]

function FooterLink({ href, external, children }: { href: string; external?: boolean; children: React.ReactNode }) {
if (external) {
return (
Expand Down Expand Up @@ -60,6 +95,9 @@ export function SEOFooter({ locale = DEFAULT_LOCALE }: { locale?: Locale } = {})
const articles = ((manifest as Record<string, unknown>).articles ?? []) as ManifestEntry[]
const resources = (manifest.resources ?? []) as ManifestEntry[]
const hasSendMoney = sendTo.length > 0 || sendFrom.length > 0
// What's left of "Resources" rides on top of "Learn More" — the column
// itself is gone, its slot in the 4-up grid taken by Legal.
const learnMoreResources = resources.filter((entry) => !RESOURCES_MOVED_ELSEWHERE.has(entry.slug))

const link = (entry: ManifestEntry) => (entry.external ? entry.href : localizeHref(entry.href, locale))

Expand Down Expand Up @@ -91,25 +129,28 @@ export function SEOFooter({ locale = DEFAULT_LOCALE }: { locale?: Locale } = {})
</FooterSection>
)}

{articles.length > 0 && (
{(articles.length > 0 || learnMoreResources.length > 0) && (
<FooterSection title={i18n.footerLearnMoreSection}>
{articles.map((entry) => (
<FooterLink key={entry.slug} href={link(entry)}>
{learnMoreResources.map((entry) => (
<FooterLink key={`resource-${entry.slug}`} href={link(entry)} external={entry.external}>
{entry.name}
</FooterLink>
))}
</FooterSection>
)}

{resources.length > 0 && (
<FooterSection title={i18n.footerResources}>
{resources.map((entry) => (
<FooterLink key={entry.slug} href={link(entry)} external={entry.external}>
{articles.map((entry) => (
<FooterLink key={entry.slug} href={link(entry)}>
{entry.name}
</FooterLink>
))}
</FooterSection>
)}

<FooterSection title={i18n.footerLegalSection}>
{LEGAL_LINKS.map((entry) => (
<FooterLink key={entry.slug} href={localizeHref(entry.href, locale)}>
{entry.label(i18n)}
</FooterLink>
))}
</FooterSection>
</div>
</nav>
)
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut is a trading name of Squirrel Labs Ltd, registered in England & Wales (No. 14558823)",
"footerSupport": "Support",
"footerDocs": "Docs",
"footerTerms": "Terms",
"footerPrivacy": "Privacy",
"footerTerms": "Terms of Service",
"footerPrivacy": "Privacy Policy",
"footerSecurity": "Security",
"footerJobs": "Jobs",
"footerSiteDirectory": "Site directory",
"footerCompare": "Compare",
"footerLearnMoreSection": "Learn More",
"footerResources": "Resources",
"footerLegalSection": "Legal",
"footerCardTermsUs": "Card Terms (U.S.)",
"footerCardTermsInternational": "Card Terms (International)",
"footerCardEsign": "E-Sign Consent",
"footerCardPrivacy": "Account Opening Privacy Notice",
"footerCardProhibitedActivities": "Prohibited Activities Policy",
"footerSendTo": "Send to {name}",
"footerSendFrom": "Send from {name}",
"footerPeanutVs": "Peanut vs {name}",
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/es-419.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut es un nombre comercial de Squirrel Labs Ltd, registrada en Inglaterra y Gales (N.º 14558823)",
"footerSupport": "Soporte",
"footerDocs": "Documentación",
"footerTerms": "Términos",
"footerPrivacy": "Privacidad",
"footerTerms": "Términos de servicio",
"footerPrivacy": "Política de privacidad",
"footerSecurity": "Seguridad",
"footerJobs": "Empleo",
"footerSiteDirectory": "Directorio del sitio",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Más información",
"footerResources": "Recursos",
"footerLegalSection": "Legal",
"footerCardTermsUs": "Términos de la tarjeta (EE. UU.)",
"footerCardTermsInternational": "Términos de la tarjeta (internacional)",
"footerCardEsign": "Consentimiento de firma electrónica",
"footerCardPrivacy": "Aviso de privacidad de apertura de cuenta",
"footerCardProhibitedActivities": "Política de actividades prohibidas",
"footerSendTo": "Enviar a {name}",
"footerSendFrom": "Enviar desde {name}",
"footerPeanutVs": "Peanut vs. {name}",
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/es-ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut es un nombre comercial de Squirrel Labs Ltd, registrada en Inglaterra y Gales (N.º 14558823)",
"footerSupport": "Soporte",
"footerDocs": "Documentación",
"footerTerms": "Términos",
"footerPrivacy": "Privacidad",
"footerTerms": "Términos de servicio",
"footerPrivacy": "Política de privacidad",
"footerSecurity": "Seguridad",
"footerJobs": "Empleo",
"footerSiteDirectory": "Directorio del sitio",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Más información",
"footerResources": "Recursos",
"footerLegalSection": "Legal",
"footerCardTermsUs": "Términos de la tarjeta (EE. UU.)",
"footerCardTermsInternational": "Términos de la tarjeta (internacional)",
"footerCardEsign": "Consentimiento de firma electrónica",
"footerCardPrivacy": "Aviso de privacidad de apertura de cuenta",
"footerCardProhibitedActivities": "Política de actividades prohibidas",
"footerSendTo": "Enviar a {name}",
"footerSendFrom": "Enviar desde {name}",
"footerPeanutVs": "Peanut vs. {name}",
Expand Down
11 changes: 8 additions & 3 deletions src/i18n/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,19 @@
"footerLegalEntity": "Peanut é um nome comercial da Squirrel Labs Ltd, registrada na Inglaterra e no País de Gales (n.º 14558823)",
"footerSupport": "Suporte",
"footerDocs": "Documentação",
"footerTerms": "Termos",
"footerPrivacy": "Privacidade",
"footerTerms": "Termos de serviço",
"footerPrivacy": "Política de privacidade",
"footerSecurity": "Segurança",
"footerJobs": "Vagas",
"footerSiteDirectory": "Diretório do site",
"footerCompare": "Comparar",
"footerLearnMoreSection": "Saiba mais",
"footerResources": "Recursos",
"footerLegalSection": "Jurídico",
"footerCardTermsUs": "Termos do cartão (EUA)",
"footerCardTermsInternational": "Termos do cartão (internacional)",
"footerCardEsign": "Consentimento de assinatura eletrônica",
"footerCardPrivacy": "Aviso de privacidade de abertura de conta",
"footerCardProhibitedActivities": "Política de atividades proibidas",
"footerSendTo": "Enviar para {name}",
"footerSendFrom": "Enviar de {name}",
"footerPeanutVs": "Peanut vs. {name}",
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ export interface Translations {
footerSiteDirectory: string
footerCompare: string
footerLearnMoreSection: string
footerResources: string
footerLegalSection: string
footerCardTermsUs: string
footerCardTermsInternational: string
footerCardEsign: string
footerCardPrivacy: string
footerCardProhibitedActivities: string
footerSendTo: string // "Send to {name}"
footerSendFrom: string // "Send from {name}"
footerPeanutVs: string // "Peanut vs {name}"
Expand Down
Loading