From e63a08c2e24dc951b4468052f2f061e462a083a3 Mon Sep 17 00:00:00 2001 From: mkfain Date: Sat, 23 May 2026 21:05:46 -0500 Subject: [PATCH] i18n: route FAQ strings through the locale namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the FAQ definition in `helpContent.ts` into two layers: * a structural template (category order, item IDs, hidden flag) that stays in TS, and * a flat `faq.*` namespace in `locales/*.json` that holds every user-visible string (category labels, questions, answer paragraph arrays). `getFAQCategories` / `getFAQItems` / `getFAQItem` keep the same `(appName)` signature and `FAQCategory` / `FAQItem` shape — internally they resolve strings through `i18n.t()`, with `{appName}` literals rewritten to `{{appName}}` for i18next interpolation. Answer arrays use `returnObjects: true` so a missing locale entry falls back to English without breaking the renderer. `HelpFAQSection` reads `i18n.language` to re-resolve on language switch; `HelpTip` calls `useTranslation()` for the same reason. The Donor / Activist guide blocks below are still keyed off the original single-brace `{appName}` literal — that's a separate i18n pass. --- src/components/HelpFAQSection.tsx | 10 +- src/components/HelpTip.tsx | 4 + src/lib/helpContent.ts | 376 ++++++++++-------------------- src/locales/en.json | 177 ++++++++++++++ 4 files changed, 315 insertions(+), 252 deletions(-) diff --git a/src/components/HelpFAQSection.tsx b/src/components/HelpFAQSection.tsx index 802ddf15..158cbd1d 100644 --- a/src/components/HelpFAQSection.tsx +++ b/src/components/HelpFAQSection.tsx @@ -1,4 +1,5 @@ import { useMemo, useState, Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; import { Accordion, @@ -77,6 +78,11 @@ export function HelpFAQSection({ listTone = 'default', }: HelpFAQSectionProps) { const { config } = useAppContext(); + const { i18n } = useTranslation(); + // `getFAQCategories` resolves strings against `i18n.language` at call + // time. Capturing the language in a local lets us include it as a memo + // dep so a language switch re-runs the resolver. + const lng = i18n.language; const filteredCategories = useMemo(() => { let cats: FAQCategory[] = getFAQCategories(config.appName); @@ -104,7 +110,9 @@ export function HelpFAQSection({ } return cats; - }, [categories, items, config.appName]); + // `lng` is in the dep list to force re-resolution on a language switch. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [categories, items, config.appName, lng]); // Tab state: first category is selected by default. const [activeTab, setActiveTab] = useState( diff --git a/src/components/HelpTip.tsx b/src/components/HelpTip.tsx index 8fe52d72..9f7c0922 100644 --- a/src/components/HelpTip.tsx +++ b/src/components/HelpTip.tsx @@ -1,5 +1,6 @@ import { HelpCircle } from 'lucide-react'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useAppContext } from '@/hooks/useAppContext'; @@ -29,6 +30,9 @@ interface HelpTipProps { */ export function HelpTip({ faqId, iconSize = 'size-4', className }: HelpTipProps) { const { config } = useAppContext(); + // Subscribing to `useTranslation()` makes the component re-render on a + // language switch so the popover content reflects the new locale. + useTranslation(); const item = getFAQItem(config.appName, faqId); if (!item) return null; diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index fd0ac843..14ad385a 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -1,19 +1,36 @@ /** * Structured FAQ content for the Help section. * - * This module is the single source of truth for all Help/FAQ data. - * Any page can call `getFAQCategories(appName)` or `getFAQItems(appName)` to - * render a full FAQ or a filtered subset (e.g. only "payments" questions on - * a wallet settings page). + * This module is the single source of truth for FAQ *structure* — category + * order, item IDs within each category, and the `hidden` flag on the + * legacy category. The user-visible strings (category labels, questions, + * and answer paragraphs) are translated and live under the `faq.*` + * namespace in `src/locales/*.json`. * - * Author-visible strings containing the app name are stored with the - * `{appName}` placeholder and substituted at read-time by the helpers. + * Any page can call `getFAQCategories(appName)` or `getFAQItems(appName)` + * to render a full FAQ or a filtered subset (e.g. only "payments" + * questions on a wallet settings page). Internally these resolve strings + * through `i18n.t()`, so callers must trigger a re-render when the active + * language changes — `HelpFAQSection` and `HelpTip` do this by depending + * on `i18n.language` via `useTranslation()`. + * + * Adding a new FAQ item: + * 1. Add `{ id: 'my-new-item' }` to the relevant category's `items` here. + * 2. Add `faq.items.my-new-item.question` and `.answer` to en.json. + * 3. Translate into the other locales (or leave them — i18next falls + * back to English at runtime). + * + * Answer strings may contain the simple inline markup supported by + * `renderInlineMarkup`: `**bold**` and `[link text](url)`, plus + * `{{appName}}` for runtime interpolation of the app's name. */ +import i18n from '@/i18n'; + // ── Types ───────────────────────────────────────────────────────────────────── export interface FAQItem { - /** Stable key used for accordion state and deep-linking. */ + /** Stable key used for accordion state, deep-linking, and i18n lookups. */ id: string; /** The question (plain text). */ question: string; @@ -39,297 +56,146 @@ export interface FAQCategory { hidden?: boolean; } -// ── Data ────────────────────────────────────────────────────────────────────── +// ── Structure (no user-visible strings; all strings live in locales) ───────── + +interface FAQItemStructure { + id: string; +} + +interface FAQCategoryStructure { + id: string; + items: FAQItemStructure[]; + hidden?: boolean; +} /** - * Raw FAQ template content. Strings may contain the literal `{appName}` - * placeholder, which is substituted at read-time by `getFAQCategories()` - * and friends. + * FAQ structure: ordered list of categories and the item IDs they contain. + * Strings are resolved from `i18n` at read-time by the helpers below. */ -const FAQ_TEMPLATE: FAQCategory[] = [ - // ── About Agora ───────────────────────────────────────────────────────── +const FAQ_STRUCTURE: FAQCategoryStructure[] = [ { id: 'getting-started', - label: 'About Agora', items: [ - { - id: 'what-is-ditto', - question: 'What is {appName}?', - answer: [ - '{appName} is a platform for sending Bitcoin donations \u2014 public or private \u2014 directly to activists. There\'s no middleman, no payment processor, and no account that can be frozen.', - '{appName} is built on Nostr, so your identity isn\'t locked to this site \u2014 you own it.', - ], - }, - { - id: 'cost-to-use', - question: 'Does {appName} cost anything?', - answer: [ - '**No.** {appName} takes no platform fee. When you donate, you pay only the Bitcoin network fee that goes to miners \u2014 not to us.', - ], - }, - { - id: 'who-made-this', - question: 'Who made {appName}?', - answer: [ - '{appName} is built by [Soapbox](https://soapbox.pub), an open-source team building tools for the Nostr ecosystem, in collaboration with the [World Liberty Congress](https://worldlibertycongress.org/).', - ], - }, + { id: 'what-is-ditto' }, + { id: 'cost-to-use' }, + { id: 'who-made-this' }, ], }, - - // ── Bitcoin Donations on Agora ────────────────────────────────────────── - // Merged section combining the practical "how it works" Q&A with the - // "why we designed it this way" rationale. On-chain only — Lightning and - // zap content is intentionally absent. { id: 'payments', - label: 'Bitcoin Donations on Agora', items: [ - { - id: 'send-bitcoin-onchain', - question: 'How does sending Bitcoin work?', - answer: [ - 'When an activist creates a campaign, they choose what kinds of payments to accept: **public**, **private**, or **both**.', - '**Public** donations are real Bitcoin on-chain. They settle on the public blockchain, work in every Bitcoin wallet, and are visible to anyone.', - '**Private** donations use **silent payments** (BIP-352). They settle on-chain too, but the transaction can\'t be linked back to the activist\'s donation code \u2014 so they stay out of public donor lists and totals. The donor needs a wallet that supports silent payments \u2014 we recommend [Ditto Wallet](https://ditto.pub) or [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk).', - 'When a campaign accepts **both**, {appName} shows a single QR code that encodes both endpoints. Silent-payment wallets read it as private; other wallets fall back to the public address. Donors don\'t have to choose \u2014 their wallet picks the right path automatically.', - 'Either way, the payment goes straight to the activist. {appName} never touches the funds.', - ], - }, - { - id: 'connect-wallet', - question: 'What is the wallet on {appName}?', - answer: [ - 'Your {appName} wallet is built from your Nostr key. It can receive Bitcoin both ways \u2014 as a public address that any Bitcoin wallet can pay, and as a silent-payments code that capable wallets can pay privately. There\'s nothing to sign up for; it exists the moment you have an account.', - 'When you create a campaign, you pick whether to accept public payments, private payments, or both. To spend what you receive, see the **Activist Guide**.', - ], - }, - { - id: 'donations-are-public-general', - question: 'Are donations on {appName} public?', - answer: [ - 'It depends on which kind of payment the activist accepts.', - '**Public donations** are recorded on the Bitcoin blockchain and on Nostr. Anyone can see the amounts, timing, and addresses.', - '**Private donations** use silent payments. They\'re not publicly linkable to the activist\'s donation code, don\'t appear in donor lists, and don\'t count toward public totals.', - 'When a campaign accepts both, the donor\'s wallet decides which path to use \u2014 silent-payment-capable wallets pay privately, others pay the public address. Read the **Donor Guide** and **Activist Guide** for the full picture.', - ], - }, - { - id: 'censorship-resistance', - question: 'What does "censorship-resistant" mean here?', - answer: [ - 'No company sits between a donor and an activist. {appName} doesn\'t hold the funds and can\'t freeze the address.', - 'As long as the Bitcoin network is running, donations can be sent and received. {appName} itself going offline wouldn\'t stop them.', - ], - }, - { - id: 'why-onchain', - question: 'Why Bitcoin?', - answer: [ - 'Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.', - 'On {appName}, activists choose how to receive: **public** (a regular Bitcoin address) for maximum reach, **private** (silent payments) for unlinkable donations, or **both** so each donor\'s wallet picks the right path automatically. Donors who only have a consumer Bitcoin app can still contribute; donors with a silent-payments wallet get privacy by default.', - 'The tradeoff is that public Bitcoin transactions are visible on the blockchain and pay a miner fee. The Donor and Activist guides explain how to handle both.', - ], - }, - { - id: 'why-not-silent-payments', - question: 'Does {appName} support silent payments?', - answer: [ - 'Yes. When an activist creates a campaign they can accept silent payments alongside, or instead of, public Bitcoin payments. Silent-payment donations still settle on-chain, but the transaction can\'t be linked back to the activist\'s donation code \u2014 so they don\'t appear in public donor lists or totals.', - 'Sending a silent payment requires a wallet that supports BIP-352. Most consumer apps don\'t yet, but [Ditto Wallet](https://ditto.pub) and [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk) do.', - 'When a campaign accepts both kinds of payment, {appName} encodes them in a single QR code. Silent-payment-capable wallets pay privately; everyone else pays the public address. No donor is shut out, and no activist is forced to choose between reach and privacy.', - ], - }, - { - id: 'why-not-lightning', - question: 'Why doesn\'t {appName} use Lightning?', - answer: [ - 'Lightning requires a Lightning wallet. The easiest ones (Wallet of Satoshi, Strike, Breez) are **custodial** \u2014 a company holds the funds and can be shut down, geo-blocked, or pressured into freezing accounts. Non-custodial Lightning is technically demanding and unreliable for newcomers.', - 'We want {appName} to work for someone whose only Bitcoin experience is a regular consumer app like Cash App, Coinbase, Strike, Venmo, or PayPal. On-chain Bitcoin works with every wallet on the planet.', - ], - }, - { - id: 'why-not-rotating-addresses', - question: 'Why doesn\'t {appName} generate a new address for every donation?', - answer: [ - 'Doing this would require {appName} to act as a money-exchanging middleman \u2014 taking custody of the Bitcoin first and then forwarding it on to the activist.', - 'That would make us a money transmitter, subject to the regulations that come with that, and a single point of failure: shut down {appName}\'s server and you\'ve shut down every donation flowing through it.', - 'Instead, each user\'s donation address is derived from their Nostr public key. Donors send directly to the activist, {appName} never touches the funds, and the platform itself can\'t be turned off to censor anyone. Activists who want per-donation privacy can accept silent payments, which give the same unlinkability without anyone holding the money in the middle.', - ], - }, - { - id: 'why-not-other-crypto', - question: 'Why not Monero or another cryptocurrency?', - answer: [ - 'Bitcoin is by far the most widely adopted cryptocurrency. That means it\'s the easiest for donors to buy and send, and the easiest for activists to receive, hold, and spend.', - 'Privacy-focused coins like Monero offer different privacy tradeoffs than Bitcoin, but they\'re unsupported by most consumer apps and harder to convert back to local currency. Asking either side of a donation to first acquire a niche cryptocurrency is a barrier {appName} won\'t put in the way. For Bitcoin donations themselves, silent payments cover the unlinkability use case without leaving the Bitcoin ecosystem.', - ], - }, + { id: 'send-bitcoin-onchain' }, + { id: 'connect-wallet' }, + { id: 'donations-are-public-general' }, + { id: 'censorship-resistance' }, + { id: 'why-onchain' }, + { id: 'why-not-silent-payments' }, + { id: 'why-not-lightning' }, + { id: 'why-not-rotating-addresses' }, + { id: 'why-not-other-crypto' }, ], }, - - // ── About Nostr ───────────────────────────────────────────────────────── - // Protocol-level questions: what Nostr is, how the npub/nsec key pair - // works, and what to do with your secret key. Placed after the payments - // section so newcomers see "what is Agora / why Bitcoin" first, and only - // dig into Nostr's identity model once they care. { id: 'about-nostr', - label: 'About Nostr', items: [ - { - id: 'what-is-nostr', - question: 'What is Nostr?', - answer: [ - 'Nostr is an open network where **you** own your account, not a company. Your identity is a cryptographic key you control, not a username on someone else\'s server.', - 'On {appName}, that same key is also what your donation address is derived from \u2014 which is why you can receive Bitcoin without signing up with anyone.', - ], - }, - { - id: 'why-login-different', - question: 'Why is my sign-in so different and long?', - answer: [ - 'Instead of a username and password controlled by a company, Nostr uses a pair of cryptographic keys.', - 'Your "public key" (starts with **npub**) is your username. Your "secret key" (starts with **nsec**) is your password. The long string is what makes it virtually impossible to guess.', - ], - }, - { - id: 'lose-secret-key', - question: 'What happens if I lose my secret key?', - answer: [ - '**There is no "forgot password" button.** Nobody can reset it for you. If you lose it, your account \u2014 and any Bitcoin sitting at your donation address \u2014 is gone forever.', - '**Save your secret key somewhere safe right now.** For tips, read [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', - ], - }, - { - id: 'manage-secret-key', - question: 'Can I save my secret key in my phone\'s password manager?', - answer: [ - 'Yes. You can save it in your device\'s password manager (iCloud Keychain, 1Password, Bitwarden, etc.). On iPhone, saving it in Passwords lets you use Face ID or Touch ID to log in.', - 'For a full guide, see [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', - ], - }, + { id: 'what-is-nostr' }, + { id: 'why-login-different' }, + { id: 'lose-secret-key' }, + { id: 'manage-secret-key' }, ], }, - - // ── Hidden legacy items ───────────────────────────────────────────────── - // Kept so existing HelpTip call sites on other pages don't break, but - // excluded from the visible FAQ. {appName}'s donation flow is Bitcoin - // only (public or private via silent payments); Lightning, zaps, and the - // network/safety topics aren't part of the public help content right now. { + // Hidden legacy items: kept so existing `HelpTip` call sites on other + // pages don't break, but excluded from the default FAQ render. id: 'legacy', - label: 'Legacy', hidden: true, items: [ - { - id: 'send-bitcoin-lightning', - question: 'How does sending Bitcoin over Lightning work?', - answer: [ - 'If a recipient has a Lightning address on their profile, you can send to that. Lightning settles in seconds and fees are tiny.', - 'Lightning sends don\'t use {appName}\'s donation address \u2014 they go straight to whatever Lightning wallet the recipient set up themselves. {appName}\'s own donation flow is on-chain only.', - ], - }, - { - id: 'what-are-zaps', - question: 'What are zaps?', - answer: [ - 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow.', - ], - }, - { - id: 'fyp', - question: 'How does the feed work?', - answer: [ - 'Your feed shows campaigns and posts from people you follow. There\'s no algorithm deciding what you see.', - ], - }, - { - id: 'what-are-relays', - question: 'What are relays?', - answer: [ - 'Relays are the servers that store and deliver Nostr events \u2014 posts, donation receipts, profile info. The defaults work out of the box; you can add or remove relays in Settings > Network.', - ], - }, - { - id: 'what-are-blossom', - question: 'What are Blossom servers?', - answer: [ - 'Blossom servers store media files (campaign images, profile pictures) when you upload them. You can manage which servers you use in Settings > Network.', - ], - }, - { - id: 'report-content', - question: 'How do I report harmful content?', - answer: [ - 'Tap the three-dot menu (**...**) on any post and select "Report." You can mute or block users from the same menu.', - ], - }, - { - id: 'vs-mastodon-bluesky', - question: 'How is Nostr different from Mastodon or Bluesky?', - answer: [ - 'On Mastodon, your account lives on a specific server. On Bluesky, most accounts depend on one company. On Nostr, your identity is a key you control, and your donation address goes with you to any Nostr app.', - ], - }, - { - id: 'profile-fields', - question: 'What are profile fields?', - answer: [ - 'Profile fields let you add extra info to your profile \u2014 links, wallet addresses, music, photos, videos.', - ], - }, + { id: 'send-bitcoin-lightning' }, + { id: 'what-are-zaps' }, + { id: 'fyp' }, + { id: 'what-are-relays' }, + { id: 'what-are-blossom' }, + { id: 'report-content' }, + { id: 'vs-mastodon-bluesky' }, + { id: 'profile-fields' }, ], }, ]; // ── Helpers ─────────────────────────────────────────────────────────────────── -/** Replace all occurrences of `{appName}` in a string with the resolved value. */ -function substitute(str: string, appName: string): string { - return str.replaceAll('{appName}', appName); +/** + * Resolve a single FAQ item to its translated form. Falls back gracefully + * when an answer key is missing in the active locale — i18next returns the + * English string in that case, so missing translations just degrade to + * English without breaking the page. + */ +function resolveItem(id: string, appName: string): FAQItem { + const question = i18n.t(`faq.items.${id}.question`, { appName }); + + // `returnObjects: true` lets us pull the answer array straight out of + // the locale file. i18next will return the key path as a string if it + // can't find the entry, so guard against that and fall back to an + // empty array so the renderer doesn't explode. + const rawAnswer = i18n.t(`faq.items.${id}.answer`, { + appName, + returnObjects: true, + }); + const answer: string[] = Array.isArray(rawAnswer) + ? (rawAnswer as string[]) + : []; + + return { id, question, answer }; } -/** Substitute placeholders in a single FAQ item. */ -function substituteItem(item: FAQItem, appName: string): FAQItem { - return { - ...item, - question: substitute(item.question, appName), - answer: item.answer.map((p) => substitute(p, appName)), - }; -} +/** Resolve a category to its translated form (label + items). */ +function resolveCategory( + structure: FAQCategoryStructure, + appName: string, +): FAQCategory { + const label = i18n.t(`faq.categories.${structure.id}.label`, { appName }); + // Description is optional — currently nothing in en.json provides one, + // but the type allows it for future use. + const descriptionKey = `faq.categories.${structure.id}.description`; + const description = i18n.exists(descriptionKey) + ? i18n.t(descriptionKey, { appName }) + : undefined; -/** Substitute placeholders in a single category (questions + answers). */ -function substituteCategory(cat: FAQCategory, appName: string): FAQCategory { return { - ...cat, - label: substitute(cat.label, appName), - description: cat.description ? substitute(cat.description, appName) : undefined, - items: cat.items.map((i) => substituteItem(i, appName)), + id: structure.id, + label, + description, + hidden: structure.hidden, + items: structure.items.map((i) => resolveItem(i.id, appName)), }; } /** - * Return the full list of FAQ categories with `{appName}` placeholders - * resolved to the given `appName`. + * Return the full list of FAQ categories with strings resolved against + * the active i18n language and `{{appName}}` interpolated to `appName`. + * + * Callers that need reactivity when the user switches languages should + * pull `i18n.language` from `useTranslation()` and include it in their + * `useMemo` dependency list. */ export function getFAQCategories(appName: string): FAQCategory[] { - return FAQ_TEMPLATE.map((c) => substituteCategory(c, appName)); + return FAQ_STRUCTURE.map((c) => resolveCategory(c, appName)); } /** Flat list of every FAQ item, optionally filtered by category ID. */ export function getFAQItems(appName: string, categoryId?: string): FAQItem[] { const cats = categoryId - ? FAQ_TEMPLATE.filter((c) => c.id === categoryId) - : FAQ_TEMPLATE; - return cats.flatMap((c) => c.items).map((i) => substituteItem(i, appName)); + ? FAQ_STRUCTURE.filter((c) => c.id === categoryId) + : FAQ_STRUCTURE; + return cats.flatMap((c) => c.items).map((i) => resolveItem(i.id, appName)); } /** Look up a single FAQ item by its ID across all categories. */ export function getFAQItem(appName: string, itemId: string): FAQItem | undefined { - for (const cat of FAQ_TEMPLATE) { - const found = cat.items.find((i) => i.id === itemId); - if (found) return substituteItem(found, appName); + for (const cat of FAQ_STRUCTURE) { + if (cat.items.some((i) => i.id === itemId)) { + return resolveItem(itemId, appName); + } } return undefined; } @@ -353,8 +219,16 @@ export { TEAM_SOAPBOX as TEAM_SOAPBOX_PACK } from '@/lib/agoraDefaults'; * (`**bold**` and `[link](url)`), and the `{appName}` placeholder. Both * are resolved at read-time by the `getDonorGuideBlocks` / * `getActivistGuideBlocks` helpers below. + * + * TODO: not yet translated. The guide blocks are still keyed off the + * original `{appName}` (single-brace) literal — separate i18n pass. */ +/** Replace `{appName}` literals in a guide string with the resolved value. */ +function substitute(str: string, appName: string): string { + return str.replaceAll('{appName}', appName); +} + /** * The two payment options a campaign can offer. Used by table headers * and inline badges. diff --git a/src/locales/en.json b/src/locales/en.json index 825de16d..7e8a3110 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1724,5 +1724,182 @@ "copyFailed": "Copy failed", "copyFailedDescription": "Could not access the clipboard. Reveal the key and copy it manually." } + }, + "faq": { + "categories": { + "getting-started": { "label": "About Agora" }, + "payments": { "label": "Bitcoin Donations on Agora" }, + "about-nostr": { "label": "About Nostr" }, + "legacy": { "label": "Legacy" } + }, + "items": { + "what-is-ditto": { + "question": "What is {{appName}}?", + "answer": [ + "{{appName}} is a platform for sending Bitcoin donations — public or private — directly to activists. There's no middleman, no payment processor, and no account that can be frozen.", + "{{appName}} is built on Nostr, so your identity isn't locked to this site — you own it." + ] + }, + "cost-to-use": { + "question": "Does {{appName}} cost anything?", + "answer": [ + "**No.** {{appName}} takes no platform fee. When you donate, you pay only the Bitcoin network fee that goes to miners — not to us." + ] + }, + "who-made-this": { + "question": "Who made {{appName}}?", + "answer": [ + "{{appName}} is built by [Soapbox](https://soapbox.pub), an open-source team building tools for the Nostr ecosystem, in collaboration with the [World Liberty Congress](https://worldlibertycongress.org/)." + ] + }, + "send-bitcoin-onchain": { + "question": "How does sending Bitcoin work?", + "answer": [ + "When an activist creates a campaign, they choose what kinds of payments to accept: **public**, **private**, or **both**.", + "**Public** donations are real Bitcoin on-chain. They settle on the public blockchain, work in every Bitcoin wallet, and are visible to anyone.", + "**Private** donations use **silent payments** (BIP-352). They settle on-chain too, but the transaction can't be linked back to the activist's donation code — so they stay out of public donor lists and totals. The donor needs a wallet that supports silent payments — we recommend [Ditto Wallet](https://ditto.pub) or [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk).", + "When a campaign accepts **both**, {{appName}} shows a single QR code that encodes both endpoints. Silent-payment wallets read it as private; other wallets fall back to the public address. Donors don't have to choose — their wallet picks the right path automatically.", + "Either way, the payment goes straight to the activist. {{appName}} never touches the funds." + ] + }, + "connect-wallet": { + "question": "What is the wallet on {{appName}}?", + "answer": [ + "Your {{appName}} wallet is built from your Nostr key. It can receive Bitcoin both ways — as a public address that any Bitcoin wallet can pay, and as a silent-payments code that capable wallets can pay privately. There's nothing to sign up for; it exists the moment you have an account.", + "When you create a campaign, you pick whether to accept public payments, private payments, or both. To spend what you receive, see the **Activist Guide**." + ] + }, + "donations-are-public-general": { + "question": "Are donations on {{appName}} public?", + "answer": [ + "It depends on which kind of payment the activist accepts.", + "**Public donations** are recorded on the Bitcoin blockchain and on Nostr. Anyone can see the amounts, timing, and addresses.", + "**Private donations** use silent payments. They're not publicly linkable to the activist's donation code, don't appear in donor lists, and don't count toward public totals.", + "When a campaign accepts both, the donor's wallet decides which path to use — silent-payment-capable wallets pay privately, others pay the public address. Read the **Donor Guide** and **Activist Guide** for the full picture." + ] + }, + "censorship-resistance": { + "question": "What does \"censorship-resistant\" mean here?", + "answer": [ + "No company sits between a donor and an activist. {{appName}} doesn't hold the funds and can't freeze the address.", + "As long as the Bitcoin network is running, donations can be sent and received. {{appName}} itself going offline wouldn't stop them." + ] + }, + "why-onchain": { + "question": "Why Bitcoin?", + "answer": [ + "Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.", + "On {{appName}}, activists choose how to receive: **public** (a regular Bitcoin address) for maximum reach, **private** (silent payments) for unlinkable donations, or **both** so each donor's wallet picks the right path automatically. Donors who only have a consumer Bitcoin app can still contribute; donors with a silent-payments wallet get privacy by default.", + "The tradeoff is that public Bitcoin transactions are visible on the blockchain and pay a miner fee. The Donor and Activist guides explain how to handle both." + ] + }, + "why-not-silent-payments": { + "question": "Does {{appName}} support silent payments?", + "answer": [ + "Yes. When an activist creates a campaign they can accept silent payments alongside, or instead of, public Bitcoin payments. Silent-payment donations still settle on-chain, but the transaction can't be linked back to the activist's donation code — so they don't appear in public donor lists or totals.", + "Sending a silent payment requires a wallet that supports BIP-352. Most consumer apps don't yet, but [Ditto Wallet](https://ditto.pub) and [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk) do.", + "When a campaign accepts both kinds of payment, {{appName}} encodes them in a single QR code. Silent-payment-capable wallets pay privately; everyone else pays the public address. No donor is shut out, and no activist is forced to choose between reach and privacy." + ] + }, + "why-not-lightning": { + "question": "Why doesn't {{appName}} use Lightning?", + "answer": [ + "Lightning requires a Lightning wallet. The easiest ones (Wallet of Satoshi, Strike, Breez) are **custodial** — a company holds the funds and can be shut down, geo-blocked, or pressured into freezing accounts. Non-custodial Lightning is technically demanding and unreliable for newcomers.", + "We want {{appName}} to work for someone whose only Bitcoin experience is a regular consumer app like Cash App, Coinbase, Strike, Venmo, or PayPal. On-chain Bitcoin works with every wallet on the planet." + ] + }, + "why-not-rotating-addresses": { + "question": "Why doesn't {{appName}} generate a new address for every donation?", + "answer": [ + "Doing this would require {{appName}} to act as a money-exchanging middleman — taking custody of the Bitcoin first and then forwarding it on to the activist.", + "That would make us a money transmitter, subject to the regulations that come with that, and a single point of failure: shut down {{appName}}'s server and you've shut down every donation flowing through it.", + "Instead, each user's donation address is derived from their Nostr public key. Donors send directly to the activist, {{appName}} never touches the funds, and the platform itself can't be turned off to censor anyone. Activists who want per-donation privacy can accept silent payments, which give the same unlinkability without anyone holding the money in the middle." + ] + }, + "why-not-other-crypto": { + "question": "Why not Monero or another cryptocurrency?", + "answer": [ + "Bitcoin is by far the most widely adopted cryptocurrency. That means it's the easiest for donors to buy and send, and the easiest for activists to receive, hold, and spend.", + "Privacy-focused coins like Monero offer different privacy tradeoffs than Bitcoin, but they're unsupported by most consumer apps and harder to convert back to local currency. Asking either side of a donation to first acquire a niche cryptocurrency is a barrier {{appName}} won't put in the way. For Bitcoin donations themselves, silent payments cover the unlinkability use case without leaving the Bitcoin ecosystem." + ] + }, + "what-is-nostr": { + "question": "What is Nostr?", + "answer": [ + "Nostr is an open network where **you** own your account, not a company. Your identity is a cryptographic key you control, not a username on someone else's server.", + "On {{appName}}, that same key is also what your donation address is derived from — which is why you can receive Bitcoin without signing up with anyone." + ] + }, + "why-login-different": { + "question": "Why is my sign-in so different and long?", + "answer": [ + "Instead of a username and password controlled by a company, Nostr uses a pair of cryptographic keys.", + "Your \"public key\" (starts with **npub**) is your username. Your \"secret key\" (starts with **nsec**) is your password. The long string is what makes it virtually impossible to guess." + ] + }, + "lose-secret-key": { + "question": "What happens if I lose my secret key?", + "answer": [ + "**There is no \"forgot password\" button.** Nobody can reset it for you. If you lose it, your account — and any Bitcoin sitting at your donation address — is gone forever.", + "**Save your secret key somewhere safe right now.** For tips, read [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys)." + ] + }, + "manage-secret-key": { + "question": "Can I save my secret key in my phone's password manager?", + "answer": [ + "Yes. You can save it in your device's password manager (iCloud Keychain, 1Password, Bitwarden, etc.). On iPhone, saving it in Passwords lets you use Face ID or Touch ID to log in.", + "For a full guide, see [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys)." + ] + }, + "send-bitcoin-lightning": { + "question": "How does sending Bitcoin over Lightning work?", + "answer": [ + "If a recipient has a Lightning address on their profile, you can send to that. Lightning settles in seconds and fees are tiny.", + "Lightning sends don't use {{appName}}'s donation address — they go straight to whatever Lightning wallet the recipient set up themselves. {{appName}}'s own donation flow is on-chain only." + ] + }, + "what-are-zaps": { + "question": "What are zaps?", + "answer": [ + "Zaps are small Lightning tips on Nostr, separate from {{appName}}'s on-chain donation flow." + ] + }, + "fyp": { + "question": "How does the feed work?", + "answer": [ + "Your feed shows campaigns and posts from people you follow. There's no algorithm deciding what you see." + ] + }, + "what-are-relays": { + "question": "What are relays?", + "answer": [ + "Relays are the servers that store and deliver Nostr events — posts, donation receipts, profile info. The defaults work out of the box; you can add or remove relays in Settings > Network." + ] + }, + "what-are-blossom": { + "question": "What are Blossom servers?", + "answer": [ + "Blossom servers store media files (campaign images, profile pictures) when you upload them. You can manage which servers you use in Settings > Network." + ] + }, + "report-content": { + "question": "How do I report harmful content?", + "answer": [ + "Tap the three-dot menu (**...**) on any post and select \"Report.\" You can mute or block users from the same menu." + ] + }, + "vs-mastodon-bluesky": { + "question": "How is Nostr different from Mastodon or Bluesky?", + "answer": [ + "On Mastodon, your account lives on a specific server. On Bluesky, most accounts depend on one company. On Nostr, your identity is a key you control, and your donation address goes with you to any Nostr app." + ] + }, + "profile-fields": { + "question": "What are profile fields?", + "answer": [ + "Profile fields let you add extra info to your profile — links, wallet addresses, music, photos, videos." + ] + } + } } }