goon/mobile/src/screens/DonateScreen.tsx
goon-foss ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00

255 lines
6.6 KiB
TypeScript

import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';
import React, { useState } from 'react';
import {
Linking,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { DONATE_COINS, DonateCoin, isPlaceholder, qrPayload } from '../lib/donate';
import { theme } from '../theme';
export function DonateScreen() {
return (
<ScrollView style={styles.root} contentContainerStyle={styles.scroll}>
<View style={styles.hero}>
<Text style={styles.heroTitle}>Support Goon</Text>
<Text style={styles.heroSubtitle}>
Goon is free, open-source, and ad-free. It stays that way only because
donations cover the server, the API keys, and the time. If the app
earned you a smile, throw a coin into the bucket.
</Text>
</View>
{DONATE_COINS.map((coin) => (
<CoinCard key={coin.id} coin={coin} />
))}
<View style={styles.footnote}>
<Text style={styles.footnoteTitle}>Why crypto?</Text>
<Text style={styles.footnoteBody}>
Mainstream payment processors (Stripe, PayPal, Patreon) refuse adult
projects even open-source tooling like this one. Crypto is the only
channel that does not require us to identify our donors and does not
let third parties freeze the project mid-month.
</Text>
<Text style={[styles.footnoteBody, { marginTop: 10 }]}>
Monero is privacy-preserving by design if you care about not being
linked to this app on-chain, send XMR. Bitcoin and USDT are public
ledgers; use a fresh wallet if anonymity matters to you.
</Text>
</View>
<View style={styles.footer}>
<Text style={styles.footerLink} onPress={() => Linking.openURL('https://www.getmonero.org/get-started/')}>
New to Monero? Learn how
</Text>
<Text style={styles.footerLink} onPress={() => Linking.openURL('https://bitcoin.org/en/getting-started')}>
New to Bitcoin? Learn how
</Text>
</View>
</ScrollView>
);
}
function CoinCard({ coin }: { coin: DonateCoin }) {
const [copied, setCopied] = useState(false);
const placeholder = isPlaceholder(coin.address);
async function copy() {
if (placeholder) return;
await Clipboard.setStringAsync(coin.address);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1800);
}
return (
<View style={[styles.card, { borderColor: coin.color }]}>
<View style={styles.cardHeader}>
<View style={[styles.ticker, { backgroundColor: coin.color }]}>
<Text style={styles.tickerText}>{coin.ticker}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.coinName}>{coin.name}</Text>
<Text style={styles.coinBlurb}>{coin.blurb}</Text>
</View>
</View>
{placeholder ? (
<View style={styles.placeholderBox}>
<Text style={styles.placeholderText}>
Address not configured yet see{' '}
<Text style={{ color: theme.muted, fontWeight: '700' }}>mobile/src/lib/donate.ts</Text>
</Text>
</View>
) : (
<>
<View style={styles.qrWrap}>
<View style={styles.qrInner}>
{/* white background mandated for QR scanability across cameras */}
<QRCode value={qrPayload(coin)} size={180} backgroundColor="#fff" color="#000" />
</View>
</View>
<Pressable onPress={copy} style={({ pressed }) => [styles.addressBox, pressed && styles.addressBoxPressed]}>
<Text style={styles.addressText} selectable>
{coin.address}
</Text>
<Text style={[styles.copyHint, copied && { color: theme.good }]}>
{copied ? '✓ Copied' : 'Tap to copy'}
</Text>
</Pressable>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: theme.bg },
scroll: { padding: 16, paddingBottom: 40 },
hero: {
marginBottom: 18,
paddingHorizontal: 4,
},
heroTitle: {
color: theme.fg,
fontSize: 28,
fontWeight: '800',
letterSpacing: -0.5,
marginBottom: 8,
},
heroSubtitle: {
color: theme.muted,
fontSize: 14,
lineHeight: 20,
},
card: {
backgroundColor: theme.card,
borderWidth: 1.5,
borderRadius: 16,
padding: 16,
marginBottom: 14,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 12,
marginBottom: 14,
},
ticker: {
width: 56,
height: 56,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
tickerText: {
color: '#fff',
fontWeight: '900',
fontSize: 14,
letterSpacing: 0.5,
},
coinName: {
color: theme.fg,
fontSize: 18,
fontWeight: '700',
marginBottom: 2,
},
coinBlurb: {
color: theme.muted,
fontSize: 12,
lineHeight: 16,
},
qrWrap: {
alignItems: 'center',
marginBottom: 14,
},
qrInner: {
padding: 10,
backgroundColor: '#fff',
borderRadius: 12,
},
addressBox: {
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.border,
borderRadius: 10,
padding: 12,
},
addressBoxPressed: {
backgroundColor: theme.card,
borderColor: theme.borderFocus,
},
addressText: {
color: theme.fg,
fontFamily: 'monospace',
fontSize: 12,
lineHeight: 17,
},
copyHint: {
color: theme.mutedDim,
fontSize: 11,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.8,
marginTop: 8,
textAlign: 'right',
},
placeholderBox: {
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.warn,
borderStyle: 'dashed',
borderRadius: 10,
padding: 14,
},
placeholderText: {
color: theme.warn,
fontSize: 12,
lineHeight: 17,
},
footnote: {
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.border,
borderRadius: 14,
padding: 16,
marginTop: 6,
},
footnoteTitle: {
color: theme.fg,
fontSize: 13,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
},
footnoteBody: {
color: theme.muted,
fontSize: 13,
lineHeight: 19,
},
footer: {
marginTop: 18,
alignItems: 'center',
gap: 10,
},
footerLink: {
color: theme.accent,
fontSize: 13,
fontWeight: '600',
},
});