Compliance requirements affect onboarding directly. If your app could be used by children, you need COPPA compliance. If you serve EU users, GDPR consent must be collected during onboarding. Age gating, parental consent, data processing agreements, and privacy notices all need to be integrated into the onboarding flow without destroying the user experience.
For security practices, see Security Best Practices for Fintech Deep Links. For privacy-safe measurement, see Attribution Data Clean Rooms: Privacy-Safe Measurement. For general onboarding best practices alongside compliance, see Onboarding Best Practices for Mobile Apps in 2026.
Age Gating
When Age Gating Is Required
| Regulation | Applies When | Age Threshold |
|---|---|---|
| COPPA (US) | App collects data from children | Under 13 |
| GDPR (EU) | App processes data of minors | Under 16 (varies by member state, 13-16) |
| AADC (UK) | App likely accessed by children | Under 18 |
| LGPD (Brazil) | App processes children's data | Under 18 (or 12 for "children" vs. "adolescents") |
| PIPL (China) | App processes minor's data | Under 14 |
Age Gate Implementation
A compliant age gate collects the date of birth without suggesting the "correct" answer:
function AgeGate({ onVerified }) {
const [birthDate, setBirthDate] = useState(null);
function handleSubmit() {
if (birthDate === null) return;
const age = calculateAge(birthDate);
if (age < 13) {
// COPPA: Cannot collect data without parental consent
navigation.navigate('ParentalConsent');
return;
}
if (age < 16) {
// GDPR: May need parental consent depending on jurisdiction
navigation.navigate('MinorOnboarding', { age });
return;
}
onVerified({ age, birthDate });
}
return (
<Screen>
<Heading>Enter your date of birth</Heading>
<Text>We need this to provide the right experience for you.</Text>
<DatePicker
value={birthDate}
onChange={setBirthDate}
maximumDate={new Date()} // Cannot be in the future
// Do NOT set a minimum date that hints at the required age
/>
<Button onPress={handleSubmit} disabled={birthDate === null}>
Continue
</Button>
</Screen>
);
}
Anti-Gaming Measures
Prevent users from lying about their age:
async function handleAgeVerification(userId, birthDate) {
// Store the attempt
await logAgeVerification(userId, birthDate);
// Check for repeated attempts (user going back and changing DOB)
const attempts = await getAgeVerificationAttempts(userId);
if (attempts.length > 1) {
// User tried to change their age — use the FIRST attempt
// This prevents children from going back and entering an older date
birthDate = attempts[0].birthDate;
}
// Persist permanently (cannot be changed in settings)
await setUserBirthDate(userId, birthDate, { locked: true });
}
COPPA Compliance
What COPPA Requires
For users under 13 in the United States:
- Verifiable parental consent before collecting, using, or disclosing personal information
- Privacy notice specifically for parents
- Limited data collection (only what's necessary)
- Parental access to the child's information
- Ability to delete the child's data
Parental Consent Flow
function ParentalConsentFlow({ childAge }) {
const [step, setStep] = useState('notice');
return (
<View>
{step === 'notice' && (
<ParentalNotice
onContinue={() => setStep('verification')}
>
<Text>
Because you are under 13, a parent or guardian must
give permission for you to use [App].
</Text>
<Text>
We will collect your parent's email to send a consent request.
</Text>
</ParentalNotice>
)}
{step === 'verification' && (
<ParentalVerification
onSubmit={async (parentEmail) => {
await sendParentalConsentEmail(parentEmail);
setStep('waiting');
}}
/>
)}
{step === 'waiting' && (
<WaitingScreen>
<Text>
We sent a consent request to your parent's email.
You can use [App] once they approve.
</Text>
</WaitingScreen>
)}
</View>
);
}
COPPA-Compliant Data Collection
const COPPA_ALLOWED_DATA = {
// Can collect without parental consent (for internal use only)
persistent_identifier: true, // For app functionality
// Requires parental consent
name: 'requires_consent',
email: 'requires_consent',
location: 'requires_consent',
photos: 'requires_consent',
// Never collect from children
behavioral_advertising_data: 'prohibited',
};
function getOnboardingFieldsForAge(age) {
if (age < 13) {
// Minimal onboarding, no personal data collection
return {
fields: ['display_name'], // Pseudonymous
skip: ['email', 'phone', 'location', 'avatar_upload'],
requireParentalConsent: true,
};
}
return { fields: 'all', skip: [], requireParentalConsent: false };
}
GDPR Consent in Onboarding
Consent Requirements
GDPR requires that consent be:
- Freely given (not bundled with account creation)
- Specific (separate consents for different purposes)
- Informed (clear language about what happens with the data)
- Unambiguous (affirmative action, no pre-checked boxes)
Consent Collection
function GDPRConsentScreen({ onComplete }) {
const [consents, setConsents] = useState({
terms: false, // Required for service
privacy: false, // Required for service
analytics: false, // Optional
marketing_email: false, // Optional
marketing_push: false, // Optional
thirdPartySharing: false, // Optional
});
const requiredConsents = ['terms', 'privacy'];
const canContinue = requiredConsents.every(key => consents[key]);
return (
<Screen>
<Heading>Your Privacy Choices</Heading>
{/* Required consents */}
<Section title="Required">
<ConsentCheckbox
label="I accept the Terms of Service"
link="/terms"
checked={consents.terms}
onChange={(v) => setConsents({ ...consents, terms: v })}
/>
<ConsentCheckbox
label="I accept the Privacy Policy"
link="/privacy"
checked={consents.privacy}
onChange={(v) => setConsents({ ...consents, privacy: v })}
/>
</Section>
{/* Optional consents */}
<Section title="Optional">
<ConsentCheckbox
label="Help improve [App] with usage analytics"
description="We collect anonymous usage data to improve the app experience."
checked={consents.analytics}
onChange={(v) => setConsents({ ...consents, analytics: v })}
/>
<ConsentCheckbox
label="Receive marketing emails"
description="Product updates, tips, and offers. Unsubscribe anytime."
checked={consents.marketing_email}
onChange={(v) => setConsents({ ...consents, marketing_email: v })}
/>
<ConsentCheckbox
label="Receive push notifications for offers"
checked={consents.marketing_push}
onChange={(v) => setConsents({ ...consents, marketing_push: v })}
/>
</Section>
<Button
onPress={() => {
saveConsents(consents);
onComplete(consents);
}}
disabled={canContinue === false}
>
Continue
</Button>
</Screen>
);
}
Storing Consent Records
GDPR requires you to prove consent was given:
async function saveConsents(userId, consents) {
const record = {
userId,
consents,
timestamp: new Date().toISOString(),
appVersion: getAppVersion(),
consentVersion: '2.1', // Version of consent text
ipAddress: getClientIP(), // For audit trail
method: 'onboarding_screen',
};
await consentLog.create(record);
}
Deep Links and Compliance
Age Gate Bypass Prevention
Deep links must not bypass age gates or consent flows:
function handleDeepLink(url) {
const user = getCurrentUser();
// Check if age gate is required
if (user.ageVerified === false) {
pendingDeepLink.save(url);
navigation.navigate('AgeGate');
return;
}
// Check if GDPR consent is collected
if (user.region === 'EU' && user.gdprConsentCollected === false) {
pendingDeepLink.save(url);
navigation.navigate('GDPRConsent');
return;
}
processDeepLink(url);
}
Consent-Aware Deep Link Parameters
GDPR and privacy regulations affect how deep link data is handled for attribution. For more on this topic, see Attribution and GDPR and Deep Linking and Privacy.
Don't pass data in deep links that requires consent you haven't collected:
function processDeepLinkParams(params, consents) {
const processedParams = {};
// Always process these (necessary for service)
processedParams.ref = params.ref;
processedParams.product = params.product;
// Only process if analytics consent given
if (consents.analytics) {
processedParams.utm_source = params.utm_source;
processedParams.utm_campaign = params.utm_campaign;
}
// Only store if marketing consent given
if (consents.marketing_email) {
processedParams.email = params.email;
}
return processedParams;
}
iOS App Tracking Transparency
ATT in Onboarding
If you track users across apps or websites (for advertising attribution), you must show the ATT prompt on iOS:
async function handleATTConsent() {
const status = await requestTrackingPermission();
switch (status) {
case 'authorized':
// Full attribution tracking
enableFullAttribution();
break;
case 'denied':
case 'restricted':
// Limited attribution (SKAdNetwork only)
enableLimitedAttribution();
break;
case 'not-determined':
// User hasn't decided yet
break;
}
}
When to Show ATT
Don't show ATT on the first screen. Wait until the user has experienced value:
function shouldShowATT(user) {
// Don't show before the user has completed basic onboarding
if (user.onboardingStep < 3) return false;
// Don't show if already determined
if (user.attStatus !== 'not-determined') return false;
// Show after the user has experienced value
if (user.hasCompletedFirstAction) return true;
return false;
}
Compliance Checklist for Onboarding
- Age gate implemented (does not hint at correct answer)
- Age verification attempts logged and locked
- Parental consent flow for users under 13 (COPPA)
- GDPR consent collected before data processing (EU users)
- Consent records stored with timestamps and versions
- Optional consents not pre-checked
- Deep links cannot bypass age gates or consent flows
- ATT prompt shown at appropriate time (iOS)
- Data collection minimized for minor users
- Privacy policy and terms accessible during onboarding
- Consent withdrawal mechanism available in settings
For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.
Get deep linking tips in your inbox
One email per week. No spam.