diff --git a/src/components/Global/FAQs/index.tsx b/src/components/Global/FAQs/index.tsx index a7af0ca91..a21732554 100644 --- a/src/components/Global/FAQs/index.tsx +++ b/src/components/Global/FAQs/index.tsx @@ -1,11 +1,15 @@ 'use client' +import type { ReactNode } from 'react' + export type FAQsProps = { heading: string questions: Array<{ id: string question: string answer: string + /** Rich JSX answer body — rendered instead of `answer`, which still feeds SEO schemas */ + answerContent?: ReactNode redirectUrl?: string redirectText?: string calModal?: boolean @@ -54,7 +58,7 @@ export function FAQsPanel({ heading, questions }: FAQsProps) {
-

{linkifyText(faq.answer)}

+ {faq.answerContent ??

{linkifyText(faq.answer)}

} {faq.calModal && ( + faqData.questions.map((q) => + q.id === SUPPORTED_RAILS_FAQ_ID ? { ...q, answerContent: } : q + ), + [faqData.questions] + ) + const [buttonVisible, setButtonVisible] = useState(true) const [isScrollFrozen, setIsScrollFrozen] = useState(false) const [buttonScale, setButtonScale] = useState(1) @@ -215,7 +228,7 @@ export function LandingPageClient({ - + {footerSlot} diff --git a/src/components/LandingPage/SupportedRailsFaqAnswer.tsx b/src/components/LandingPage/SupportedRailsFaqAnswer.tsx new file mode 100644 index 000000000..2777fbcef --- /dev/null +++ b/src/components/LandingPage/SupportedRailsFaqAnswer.tsx @@ -0,0 +1,56 @@ +'use client' + +import ChainChip from '@/components/AddMoney/components/ChainChip' +import { CHAIN_LOGOS, OTHER_SUPPORTED_CHAINS, SUPPORTED_EVM_CHAINS, getSupportedTokens } from '@/constants/rhino.consts' +import { FIAT_RAILS } from '@/constants/faq.consts' +import { chainDisplayName } from '@/utils/chain-display.utils' + +/** + * Rich answer body for the "which networks, tokens and banks?" landing FAQ item. + * Renders from the same rhino.consts constants as the add-money Choose Network + * drawer (and FIAT_RAILS shared with the plain-text SEO answer), so the FAQ + * always advertises exactly what the app supports. + */ +export function SupportedRailsFaqAnswer() { + return ( +
+
+

+ Crypto — one deposit address for all {SUPPORTED_EVM_CHAINS.length} EVM networks, plus{' '} + {OTHER_SUPPORTED_CHAINS.map(chainDisplayName).join(' and ')}: +

+
+ {[...SUPPORTED_EVM_CHAINS, ...OTHER_SUPPORTED_CHAINS].map((chain) => ( + + ))} +
+
+
+

Tokens:

+
+ {getSupportedTokens('EVM').map((token) => ( + + ))} +
+

+ USDC & USDT on every network · ETH on EVM networks · Tron is USDT-only +

+
+
+

Banks & local payment apps:

+
    + {FIAT_RAILS.map((rail) => ( +
  • + {rail.flag} + {rail.name} + + {rail.currency} · {rail.region} + +
  • + ))} +
+
+

Deposits are free — Peanut covers the gas.

+
+ ) +} diff --git a/src/constants/faq.consts.ts b/src/constants/faq.consts.ts new file mode 100644 index 000000000..327cd1c93 --- /dev/null +++ b/src/constants/faq.consts.ts @@ -0,0 +1,32 @@ +import { OTHER_SUPPORTED_CHAINS, SUPPORTED_EVM_CHAINS } from '@/constants/rhino.consts' +import { chainDisplayName } from '@/utils/chain-display.utils' + +/** + * The "which networks, tokens and banks?" landing FAQ item. Question, fiat-rail + * facts and plain-text answer live here (server-safe, feeds the FAQPage JSON-LD + * via getLandingContent); the rich chip UI is rendered client-side by + * SupportedRailsFaqAnswer, matched by id. Chain names derive from rhino.consts + * and the visible rails render from FIAT_RAILS, so the public answer and the + * on-page UI share one source and can't drift from what the app supports. + */ +export const SUPPORTED_RAILS_FAQ_ID = 'supported-rails' + +export const FIAT_RAILS = [ + { flag: '🇺🇸', name: 'ACH & Wire', currency: 'USD', region: 'United States' }, + { flag: '🇪🇺', name: 'SEPA', currency: 'EUR', region: '36 countries' }, + { flag: '🇬🇧', name: 'Faster Payments', currency: 'GBP', region: 'United Kingdom' }, + { flag: '🇲🇽', name: 'SPEI', currency: 'MXN', region: 'Mexico' }, + { flag: '🇦🇷', name: 'Mercado Pago', currency: 'ARS', region: 'Argentina' }, + { flag: '🇧🇷', name: 'Pix', currency: 'BRL', region: 'Brazil' }, +] as const + +const EVM_CHAIN_LIST = SUPPORTED_EVM_CHAINS.map(chainDisplayName).join(', ') +const OTHER_CHAIN_LIST = OTHER_SUPPORTED_CHAINS.map(chainDisplayName).join(' and ') +const FIAT_RAIL_LIST = FIAT_RAILS.map((rail) => `${rail.name} (${rail.currency}, ${rail.region})`).join(', ') + +export const SUPPORTED_RAILS_FAQ_QUESTION = 'Which networks, tokens and banks does Peanut support?' + +export const SUPPORTED_RAILS_FAQ_ANSWER = + `Crypto: deposit and withdraw USDC and USDT on ${SUPPORTED_EVM_CHAINS.length} EVM networks with a single address (${EVM_CHAIN_LIST}), plus ${OTHER_CHAIN_LIST}. ETH is also supported on EVM networks; Tron is USDT-only. ` + + `Banks & local payment apps: ${FIAT_RAIL_LIST}. ` + + 'Deposits are free — Peanut covers the gas.' diff --git a/src/lib/landingContent.ts b/src/lib/landingContent.ts index 58984f8cb..95c77a853 100644 --- a/src/lib/landingContent.ts +++ b/src/lib/landingContent.ts @@ -5,6 +5,11 @@ // component or route handler, never inside a 'use client' file. import { readSingletonContentLocalized } from '@/lib/content' +import { + SUPPORTED_RAILS_FAQ_ANSWER, + SUPPORTED_RAILS_FAQ_ID, + SUPPORTED_RAILS_FAQ_QUESTION, +} from '@/constants/faq.consts' import type { Locale } from '@/i18n/types' interface LandingFrontmatter { @@ -40,7 +45,30 @@ const DEFAULTS: LandingContent = { marqueeMessages: [], } +// Code-defined FAQ item advertising supported networks/tokens/bank rails. +// Lives in code (not the content MD) because its facts derive from +// rhino.consts — the same constants the add-money flow renders — so the +// public answer can't drift from what the app actually supports. +// LandingPageClient swaps in the rich chip UI by this id. +const SUPPORTED_RAILS_QUESTION = { + id: SUPPORTED_RAILS_FAQ_ID, + question: SUPPORTED_RAILS_FAQ_QUESTION, + answer: SUPPORTED_RAILS_FAQ_ANSWER, +} + +// Insert right after the "What is Peanut?" question (falls back to the end). +function withSupportedRails(questions: LandingContent['faqData']['questions']) { + const idx = questions.findIndex((q) => /what is peanut\??/i.test(q.question)) + const at = idx === -1 ? questions.length : idx + 1 + return [...questions.slice(0, at), SUPPORTED_RAILS_QUESTION, ...questions.slice(at)] +} + export function getLandingContent(locale: Locale = 'en'): LandingContent { + const base = readLandingContent(locale) + return { ...base, faqData: { ...base.faqData, questions: withSupportedRails(base.faqData.questions) } } +} + +function readLandingContent(locale: Locale): LandingContent { const content = readSingletonContentLocalized('landing', locale) if (!content) return DEFAULTS diff --git a/src/utils/chain-display.utils.ts b/src/utils/chain-display.utils.ts new file mode 100644 index 000000000..fe9bce425 --- /dev/null +++ b/src/utils/chain-display.utils.ts @@ -0,0 +1,8 @@ +// Display labels where plain title-case reads wrong. +const CHAIN_DISPLAY_OVERRIDES: Record = { + BNB: 'BNB Chain', +} + +/** rhino.consts chain key (e.g. 'ARBITRUM', 'BNB') → human display name */ +export const chainDisplayName = (chain: string): string => + CHAIN_DISPLAY_OVERRIDES[chain] ?? chain.charAt(0) + chain.slice(1).toLowerCase()