Skip to content
Tolinku
Tolinku
Sign In Start Free
Use Cases · · 6 min read

Onboarding Compliance: COPPA, GDPR, and Age Gating

By Tolinku Staff
|
Tolinku fintech deep linking dashboard screenshot for use cases blog posts

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:

  1. Verifiable parental consent before collecting, using, or disclosing personal information
  2. Privacy notice specifically for parents
  3. Limited data collection (only what's necessary)
  4. Parental access to the child's information
  5. Ability to delete the child's data
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 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)
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>
  );
}

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);
}

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);
}

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.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.