FONTY:
- Dodane assets/fonts/: GeneralSans Regular/Medium/Semibold (Fontshare, free
commercial) + GeistMono Regular (Vercel OFL). Pobrane jako .ttf.
- expo-font ~13.0.4 (matchuje SDK 52). Native module jest w APK bo `expo`
ciagnie expo-font jako bezposrednia zaleznosc -> useFonts dziala przez OTA
bez rebuildu.
- App.tsx: useFonts() gate (blokuje render do zaladowania, .ttf z bundla <100ms)
+ globalny Text.defaultProps fontFamily=GeneralSans-Regular dla body.
- theme.ts: fonts = { body, medium, display, mono }. RN nie syntezuje weightow
dla custom fontow, wiec 4 osobne rodziny per-weight (gotcha udokumentowany).
- Jawne fonty na high-traffic: SceneTile (title=display, meta+dur=mono),
MoviePosterCard (j.w.), navigation (taby display/medium, header display).
LOGO:
- GoonWordmark przepisany: zamiast krzywych recznych SVG path (o-ka jako
nachodzace elipsy, zniekształcone n) renderuje PRAWDZIWY tekst w General Sans
Semibold. Dwutonowy twist: "g[oo]n" ze srodkowym "oo" w oxblood.
- GoonMark (monogram): czysty SVG koncentryczny ring + dot (oxblood) — motyw
soczewki/oka. Dla app-icon/splash gdzie font niedostepny.
- Wpiety na AgeGate (wordmark 40), Login (mark 44 + wordmark 44), nav header.
OTA: c986c911-0868-44f7-9f4a-fc2a74e53095 live (23 assets, 4 fonty serwuja 200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
295 lines
7.2 KiB
TypeScript
295 lines
7.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
BackHandler,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { GoonWordmark } from '../components/GoonWordmark';
|
|
import { markAccepted } from '../lib/agegate';
|
|
import { theme } from '../theme';
|
|
|
|
interface Props {
|
|
onAccept: () => void;
|
|
}
|
|
|
|
/** First-launch (and post-ToS-bump) gate. Blocks access to the app until user
|
|
* affirms they're 18+ AND accepts the Disclaimer/Terms.
|
|
*
|
|
* Decline = exit app (Android: BackHandler.exitApp; iOS: cannot programmatically
|
|
* close, so we show a "please uninstall" sentinel — covered by `declined` state).
|
|
*
|
|
* The body text is intentionally short and non-legalese; the canonical text is
|
|
* in DISCLAIMER.md and linked via "Read full disclaimer".
|
|
*/
|
|
export function AgeGateScreen({ onAccept }: Props) {
|
|
const [ageOk, setAgeOk] = useState(false);
|
|
const [tosOk, setTosOk] = useState(false);
|
|
const [declined, setDeclined] = useState(false);
|
|
|
|
const canContinue = ageOk && tosOk;
|
|
|
|
const onDecline = () => {
|
|
if (Platform.OS === 'android') {
|
|
BackHandler.exitApp();
|
|
} else {
|
|
setDeclined(true);
|
|
}
|
|
};
|
|
|
|
const onContinue = async () => {
|
|
await markAccepted();
|
|
onAccept();
|
|
};
|
|
|
|
if (declined) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.declinedBox}>
|
|
<Text style={styles.title}>Access denied</Text>
|
|
<Text style={styles.body}>
|
|
You must accept the Disclaimer and confirm you are at least 18 years
|
|
old to use Goon. Please close and uninstall the app.
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<ScrollView
|
|
contentContainerStyle={styles.scroll}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<GoonWordmark size={40} />
|
|
<Text style={styles.subtitle}>Adult content · Self-hosted only</Text>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.heading}>Before you continue</Text>
|
|
<Text style={styles.body}>
|
|
Goon is an aggregator for adult-content scene metadata. It indexes
|
|
information from third-party sources and links to those sources for
|
|
playback. The app itself contains no media.
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.heading}>You must affirm</Text>
|
|
<Text style={styles.bullet}>
|
|
• You are at least 18 years old, or the age of majority in your
|
|
jurisdiction (whichever is greater).
|
|
</Text>
|
|
<Text style={styles.bullet}>
|
|
• Adult content is legal to access and store metadata about where
|
|
you live.
|
|
</Text>
|
|
<Text style={styles.bullet}>
|
|
• You will use this app only on a device under your control. Do not
|
|
share access with minors.
|
|
</Text>
|
|
<Text style={styles.bullet}>
|
|
• You understand that scene metadata, thumbnails, and links are
|
|
sourced from third parties and may be inaccurate or unavailable.
|
|
</Text>
|
|
</View>
|
|
|
|
<Text style={styles.fineprint}>
|
|
The full Disclaimer and Terms are shipped in the project repository
|
|
(DISCLAIMER.md). By continuing, you accept those terms.
|
|
</Text>
|
|
|
|
<View style={styles.checks}>
|
|
<CheckRow
|
|
checked={ageOk}
|
|
onToggle={() => setAgeOk((v) => !v)}
|
|
label="I am at least 18 years old (or the age of majority in my jurisdiction)."
|
|
/>
|
|
<CheckRow
|
|
checked={tosOk}
|
|
onToggle={() => setTosOk((v) => !v)}
|
|
label="I have read and accept the Disclaimer and Terms."
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.actions}>
|
|
<Pressable
|
|
style={[styles.btn, styles.btnDecline]}
|
|
onPress={onDecline}
|
|
>
|
|
<Text style={styles.btnDeclineText}>Decline</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={[styles.btn, styles.btnContinue, !canContinue && styles.btnDisabled]}
|
|
disabled={!canContinue}
|
|
onPress={onContinue}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.btnContinueText,
|
|
!canContinue && styles.btnContinueTextDisabled,
|
|
]}
|
|
>
|
|
Continue
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
function CheckRow({
|
|
checked,
|
|
onToggle,
|
|
label,
|
|
}: {
|
|
checked: boolean;
|
|
onToggle: () => void;
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<Pressable style={styles.checkRow} onPress={onToggle} hitSlop={6}>
|
|
<View style={[styles.checkbox, checked && styles.checkboxOn]}>
|
|
{checked ? <Text style={styles.checkboxMark}>✓</Text> : null}
|
|
</View>
|
|
<Text style={styles.checkLabel}>{label}</Text>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.bg,
|
|
},
|
|
scroll: {
|
|
paddingHorizontal: 24,
|
|
paddingTop: 32,
|
|
paddingBottom: 48,
|
|
},
|
|
declinedBox: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: 32,
|
|
},
|
|
title: {
|
|
color: theme.fg,
|
|
fontSize: 36,
|
|
fontWeight: '700',
|
|
letterSpacing: 1,
|
|
},
|
|
subtitle: {
|
|
color: theme.muted,
|
|
fontSize: 14,
|
|
marginTop: 4,
|
|
marginBottom: 28,
|
|
letterSpacing: 0.5,
|
|
},
|
|
section: {
|
|
marginBottom: 22,
|
|
},
|
|
heading: {
|
|
color: theme.fg,
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
marginBottom: 8,
|
|
},
|
|
body: {
|
|
color: theme.fg,
|
|
fontSize: 14,
|
|
lineHeight: 21,
|
|
},
|
|
bullet: {
|
|
color: theme.fg,
|
|
fontSize: 14,
|
|
lineHeight: 21,
|
|
marginBottom: 6,
|
|
},
|
|
fineprint: {
|
|
color: theme.muted,
|
|
fontSize: 12,
|
|
lineHeight: 17,
|
|
marginBottom: 24,
|
|
marginTop: 4,
|
|
fontStyle: 'italic',
|
|
},
|
|
checks: {
|
|
marginBottom: 24,
|
|
gap: 14,
|
|
},
|
|
checkRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: 12,
|
|
},
|
|
checkbox: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 4,
|
|
borderWidth: 2,
|
|
borderColor: theme.border,
|
|
backgroundColor: theme.bgElevated,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginTop: 1,
|
|
},
|
|
checkboxOn: {
|
|
borderColor: theme.accent,
|
|
backgroundColor: theme.accent,
|
|
},
|
|
checkboxMark: {
|
|
color: '#fff',
|
|
fontSize: 14,
|
|
fontWeight: '800',
|
|
lineHeight: 14,
|
|
},
|
|
checkLabel: {
|
|
flex: 1,
|
|
color: theme.fg,
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
},
|
|
actions: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
marginTop: 8,
|
|
},
|
|
btn: {
|
|
flex: 1,
|
|
paddingVertical: 14,
|
|
borderRadius: 8,
|
|
alignItems: 'center',
|
|
},
|
|
btnDecline: {
|
|
backgroundColor: theme.bgElevated,
|
|
borderWidth: 1,
|
|
borderColor: theme.border,
|
|
},
|
|
btnDeclineText: {
|
|
color: theme.muted,
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
},
|
|
btnContinue: {
|
|
backgroundColor: theme.accent,
|
|
},
|
|
btnDisabled: {
|
|
backgroundColor: theme.bgElevated,
|
|
borderWidth: 1,
|
|
borderColor: theme.border,
|
|
},
|
|
btnContinueText: {
|
|
color: '#fff',
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
},
|
|
btnContinueTextDisabled: {
|
|
color: theme.mutedDim,
|
|
},
|
|
});
|