From 0af9a1f91da778c1b76378b12a006a7517c658f3 Mon Sep 17 00:00:00 2001 From: best2025j Date: Mon, 27 Jul 2026 14:32:53 +0100 Subject: [PATCH] changes made --- frontend/README.md | 11 ++ frontend/app/page.tsx | 2 + frontend/components/faq-accordion.test.tsx | 55 ++++++++ frontend/components/faq-accordion.tsx | 140 +++++++++++++++++++++ frontend/next-env.d.ts | 2 +- frontend/package-lock.json | 63 ---------- frontend/package.json | 8 ++ 7 files changed, 217 insertions(+), 64 deletions(-) create mode 100644 frontend/components/faq-accordion.test.tsx create mode 100644 frontend/components/faq-accordion.tsx diff --git a/frontend/README.md b/frontend/README.md index ee47aff..edc20bc 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -50,6 +50,17 @@ The details step (`steps/details-step.tsx`) collects the payment details: Validation runs on submit and then re-runs on every change so errors clear as soon as the input becomes valid. Errors are announced to assistive technology via `role="alert"` and linked to their fields with `aria-describedby`/`aria-invalid`. +## FAQ accordion + +The homepage now includes an accessible FAQ accordion covering: + +- What is an ephemeral account? +- Do recipients need a wallet? +- What happens if the payment is unclaimed? +- Is it safe? + +The component uses keyboard navigation and WAI-ARIA accordion patterns so users can open and close items using Enter, Space, Arrow Up/Down, Home, and End. + ## SDK wrapper (`lib/bridgelet.ts`) `lib/bridgelet.ts` is the typed entry point for the bridgelet-sdk API. The confirm step of the send form creates ephemeral accounts through it: diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 8eecf98..8e8fc1a 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { PageShell } from '@/components/page-shell'; import { HowItWorks } from '@/components/how-it-works'; import { CTABanner } from '@/components/cta-banner'; +import { FAQAccordion } from '@/components/faq-accordion'; export default function HomePage() { return ( @@ -29,6 +30,7 @@ export default function HomePage() { + ); diff --git a/frontend/components/faq-accordion.test.tsx b/frontend/components/faq-accordion.test.tsx new file mode 100644 index 0000000..bddfd39 --- /dev/null +++ b/frontend/components/faq-accordion.test.tsx @@ -0,0 +1,55 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { FAQAccordion } from './faq-accordion'; + +describe('FAQAccordion', () => { + it('renders the FAQ heading and questions', () => { + render(); + + expect(screen.getByRole('heading', { name: /frequently asked questions/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /what is an ephemeral account\?/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /do recipients need a wallet\?/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /what happens if the payment is unclaimed\?/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /is it safe\?/i })).toBeInTheDocument(); + }); + + it('keeps all panels collapsed by default except the first one', () => { + render(); + + const firstButton = screen.getByRole('button', { name: /what is an ephemeral account\?/i }); + expect(firstButton).toHaveAttribute('aria-expanded', 'true'); + + const secondButton = screen.getByRole('button', { name: /do recipients need a wallet\?/i }); + expect(secondButton).toHaveAttribute('aria-expanded', 'false'); + }); + + it('expands and collapses panels on click', async () => { + const user = userEvent.setup(); + render(); + + const secondButton = screen.getByRole('button', { name: /do recipients need a wallet\?/i }); + await user.click(secondButton); + + expect(secondButton).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByRole('region', { name: /do recipients need a wallet\?/i })).toBeVisible(); + + await user.click(secondButton); + expect(secondButton).toHaveAttribute('aria-expanded', 'false'); + }); + + it('moves focus with arrow keys and toggles panel with enter', async () => { + const user = userEvent.setup(); + render(); + + const firstButton = screen.getByRole('button', { name: /what is an ephemeral account\?/i }); + const secondButton = screen.getByRole('button', { name: /do recipients need a wallet\?/i }); + + firstButton.focus(); + await user.keyboard('{ArrowDown}'); + expect(secondButton).toHaveFocus(); + + await user.keyboard('{Enter}'); + expect(secondButton).toHaveAttribute('aria-expanded', 'true'); + }); +}); diff --git a/frontend/components/faq-accordion.tsx b/frontend/components/faq-accordion.tsx new file mode 100644 index 0000000..25f9aec --- /dev/null +++ b/frontend/components/faq-accordion.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { useState, useRef, useMemo, type KeyboardEvent } from 'react'; + +type FAQ = { + question: string; + answer: string; +}; + +const FAQS: FAQ[] = [ + { + question: 'What is an ephemeral account?', + answer: + 'An ephemeral account is a temporary Stellar account created for a single payment. It is funded by the sender, and the payment is swept to the recipient’s wallet only after the recipient claims it.', + }, + { + question: 'Do recipients need a wallet?', + answer: + 'Recipients do not need a wallet to receive the claim link or understand the payment. To complete the claim, they must submit a valid Stellar wallet address so the funds can be transferred into their own wallet.', + }, + { + question: 'What happens if the payment is unclaimed?', + answer: + 'If a payment is not claimed, the ephemeral account remains isolated and the funds stay there until the configured expiry or recovery process. This prevents the payment from being lost or mixed with other balances.', + }, + { + question: 'Is it safe?', + answer: + 'Yes. The ephemeral account is isolated for the payment, and the claim process requires a valid wallet address before funds are swept. No recipient private key is exposed by the payment creation flow.', + }, +]; + +export function FAQAccordion() { + const [openIndex, setOpenIndex] = useState(0); + const buttonRefs = useRef>([]); + + const buttonIdPrefix = 'faq-accordion-button-'; + const panelIdPrefix = 'faq-accordion-panel-'; + + const handleToggle = (index: number) => { + setOpenIndex((current) => (current === index ? null : index)); + }; + + const moveFocus = (index: number) => { + const button = buttonRefs.current[index]; + if (button) { + button.focus(); + } + }; + + const handleKeyDown = (event: KeyboardEvent, index: number) => { + const maxIndex = FAQS.length - 1; + + if (event.key === 'ArrowDown') { + event.preventDefault(); + moveFocus(index === maxIndex ? 0 : index + 1); + return; + } + + if (event.key === 'ArrowUp') { + event.preventDefault(); + moveFocus(index === 0 ? maxIndex : index - 1); + return; + } + + if (event.key === 'Home') { + event.preventDefault(); + moveFocus(0); + return; + } + + if (event.key === 'End') { + event.preventDefault(); + moveFocus(maxIndex); + return; + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleToggle(index); + } + }; + + const faqList = useMemo( + () => + FAQS.map((faq, index) => { + const isOpen = openIndex === index; + const buttonId = `${buttonIdPrefix}${index}`; + const panelId = `${panelIdPrefix}${index}`; + + return ( +
+

+ +

+ +
+ ); + }), + [openIndex], + ); + + return ( +
+
+

+ Frequently asked questions +

+

+ Answers for ephemeral accounts, wallets, claim safety, and what happens when a payment is unclaimed. +

+
+
{faqList}
+
+ ); +} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3fd33e0..d4bc3a9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,6 @@ }, "devDependencies": { "@lhci/cli": "^0.15.1", - "@playwright/test": "^1.61.1", "@storybook/nextjs-vite": "^10.4.6", "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -3312,22 +3311,6 @@ "third-party-web": "latest" } }, - "node_modules/@playwright/test": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", - "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.62.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@puppeteer/browsers": { "version": "2.13.2", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", @@ -12605,52 +12588,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.62.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index abee87d..c4ad4a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,5 +49,13 @@ }, "engines": { "node": ">=20.10.0" + }, + "allowScripts": { + "esbuild@0.28.1": true, + "esbuild@0.21.5": true, + "esbuild@0.25.12": true, + "sharp@0.34.5": true, + "msw@2.15.0": true, + "unrs-resolver@1.12.2": true } }