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 (
Access denied
You must accept the Disclaimer and confirm you are at least 18 years
old to use Goon. Please close and uninstall the app.
);
}
return (
Adult content · Self-hosted only
Before you continue
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.
You must affirm
• You are at least 18 years old, or the age of majority in your
jurisdiction (whichever is greater).
• Adult content is legal to access and store metadata about where
you live.
• You will use this app only on a device under your control. Do not
share access with minors.
• You understand that scene metadata, thumbnails, and links are
sourced from third parties and may be inaccurate or unavailable.
The full Disclaimer and Terms are shipped in the project repository
(DISCLAIMER.md). By continuing, you accept those terms.
setAgeOk((v) => !v)}
label="I am at least 18 years old (or the age of majority in my jurisdiction)."
/>
setTosOk((v) => !v)}
label="I have read and accept the Disclaimer and Terms."
/>
Decline
Continue
);
}
function CheckRow({
checked,
onToggle,
label,
}: {
checked: boolean;
onToggle: () => void;
label: string;
}) {
return (
{checked ? ✓ : null}
{label}
);
}
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,
},
});