npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-play-age-range-declaration

v3.0.5

Published

React Native Nitro Module to access Google Play's Age Signals API & Apple's Declared Age Range API

Readme

react-native-play-age-range-declaration

A React Native Nitro Module providing a unified API for age-appropriate experiences across platforms — bridging:

  • 🟢 Google Play Age Signals API (Android)
  • 🟠 Amazon Appstore GetUserAgeData API (Android)
  • 🟣 Samsung Galaxy Store Age Signals API (Android)
  • 🔵 Apple Declared Age Range API (iOS 26.2+; tested on 26.5)

[!IMPORTANT]

  • Works in both Expo & Bare (Non Expo) React Native projects.

📦 Installation

npm install react-native-play-age-range-declaration react-native-nitro-modules

[!NOTE]

  • Apple's Declared Age Range API requires iOS 26.2+ (tested on iOS 26.5); Google Play Age Signals requires Android 15+ with the Play Store installed (both tested on real devices — see the demos below).
  • Uses com.google.android.play:age-signals 0.0.4, which splits the old userStatus into ageRangeSource + significantChangeStatus. The library maps this back to the same PlayAgeSignalsUserStatus enum, so the JS API is unchanged.
  • Amazon Appstore and Samsung Galaxy Store age signals respond when the app is installed from that store. Every store can also be mocked from JS for development — see TESTING_ANDROID.md.

Demo


🧠 Overview

| Store | API Used | Purpose | | ------------------------ | ------------------------------------------------------------ | -------------------------------------------------- | | Google Play | Play Age Signals API (com.google.android.play:age-signals) | Detect user supervision / verified status | | Amazon Appstore | GetUserAgeData ContentProvider | Age range + parental-consent status | | Samsung Galaxy Store | ASAA getAgeSignalResult ContentProvider | Detect user supervision / verified status | | Apple App Store | Declared Age Range API (AgeRangeService.requestAgeRange) | Get user’s declared age range (e.g., 13–15, 16–17) |

The store the app was installed from is detected natively (detectStore()), and getIsConsideredOlderThan routes to the matching API automatically.


Configuration

iOS: Add the below entitlement to your project:

com.apple.developer.declared-age-range

Android: No extra configuration is needed — the library's manifest already declares the Samsung ASAA permission and the <queries> entries for the Amazon and Samsung ContentProviders. Each real API only responds when the app was installed from that store (Play Store, Amazon Appstore, or Galaxy Store).

Testing & Mocking

Every store's API can be mocked from JS, so you can develop and test on any device or emulator — see TESTING_ANDROID.md.


⚙️ Usage

Initialize the package

[!NOTE] > iOS Only: setAgeRangeThresholds is only required for iOS. Android does not require this initialization.

Before using getIsConsideredOlderThan on iOS, or getAppleDeclaredAgeRangeStatus you must initialize the package by calling setAgeRangeThresholds with an array of age thresholds. This should include the different ages that gate content in your app

import { setAgeRangeThresholds } from 'react-native-play-age-range-declaration';

setAgeRangeThresholds([13, 15]);

Requirements:

  • First threshold is required
  • Values must be between 1 and 18 (inclusive)
  • Values must be in ascending order
  • Values must be at least 2 years apart

Examples:

setAgeRangeThresholds([13]); // Single threshold at 13
setAgeRangeThresholds([13, 15]); // Two thresholds at 13 and 15
setAgeRangeThresholds([13, 15, 17]); // Three thresholds at 13, 15, and 17

[!WARNING] > iOS Permission Re-prompt: If you change the age thresholds after the user has already granted permission, iOS will re-prompt the user for permission. It's recommended to set the thresholds once at app startup and keep them consistent.

Checking if the user is allowed to access age-gated content

Use getIsConsideredOlderThan to check if a user is allowed to access age-gated content. This function works on both iOS and Android and returns true if the user is considered older than the specified age.

import { getIsConsideredOlderThan } from 'react-native-play-age-range-declaration';

// Check if user is older than 18
const canAccessGatedContent = await getIsConsideredOlderThan(16);

if (canAccessGatedContent) {
  // Show 16+ content
} else {
  // Show age restriction message
}

Behavior:

  • Detects the installing store natively and queries that store's age API (Google Play is used when the installer is unknown)
  • Returns true if the user is not in an applicable region where the store is legally required to provide age signals
  • Returns true if the user's age-range lower bound (verified, declared, or set by a parent) is at or above the specified age
  • Returns true if the store call failed — the check fails open so the app keeps working
  • Returns false in all other cases (age unknown in an applicable region, parental consent revoked, or age range below the threshold)

Example: Gating content based on age

const [isLoading, setIsLoading] = useState<boolean>(false);
const [canAccessContent, setCanAccessContent] = useState<boolean>(false);

const checkAgeRestriction = async () => {
  const getIsConsideredOlderThan18 = await getIsConsideredOlderThan(18);
  setCanAccessContent(getIsConsideredOlderThan18);
};

// In your component
{
  !isLoading && canAccessContent ? (
    <AgeGatedContent />
  ) : (
    <AgeRestrictionMessage />
  );
}

iOS: Checking eligibility before requesting the sheet

Apple's Declared Age Range API is only available to eligible users (iOS 26.2+, applicable region, signed-in Apple Account, etc.). The library exposes eligibility as a separate call from the request itself, so you can decide what to do on ineligible devices without triggering any UI:

import {
  isEligibleForAppleAgeRange,
  isEligibleForAppleAgeRangeInAppStore,
  getAppleDeclaredAgeRangeStatus,
} from 'react-native-play-age-range-declaration';

// Pure pre-flight check — no UI, no side effects.
// false on Android and on iOS < 26.2.
const eligible = await isEligibleForAppleAgeRange();

// Same, but also gates on the app being installed from the App Store —
// matches the conditions getIsConsideredOlderThan uses internally.
const eligibleInAppStore = await isEligibleForAppleAgeRangeInAppStore();

if (eligible) {
  const result = await getAppleDeclaredAgeRangeStatus();
  // …use result
}

Why two functions? getAppleDeclaredAgeRangeStatus does not pre-check eligibility — it always tries to present the sheet on iOS 26.2+. If the device is ineligible or on an older iOS, it resolves to { isEligible: false, ... } rather than throwing. This lets callers who want to always prompt (e.g. re-request after settings change) bypass the gate, and callers who want the gate can call isEligibleForAppleAgeRange() first.

getIsConsideredOlderThan is unaffected — it still uses the store gate internally.

Full Example

The example below fetches the current store's status plus three age gates. For a complete demo with an Android store selector and every mock scenario, see example/src/App.tsx.

import { useState } from 'react';
import {
  Text,
  View,
  StyleSheet,
  ActivityIndicator,
  ScrollView,
  Platform,
  Pressable,
} from 'react-native';

import {
  getGooglePlayAgeRangeStatus,
  getAppleDeclaredAgeRangeStatus,
  type AgeSignalsResult,
  type DeclaredAgeRangeResult,
  AgeSignalsUserStatusString,
  getIsConsideredOlderThan,
  setAgeRangeThresholds,
} from 'react-native-play-age-range-declaration';

setAgeRangeThresholds([13, 15]);

export default function App() {
  const [androidResult, setAndroidResult] = useState<AgeSignalsResult | null>(
    null
  );

  const [appleResult, setAppleResult] = useState<DeclaredAgeRangeResult | null>(
    null
  );

  const [isConsideredOlderThan18, setIsConsideredOlderThan18] = useState<
    boolean | null
  >(null);
  const [isConsideredOlderThan15, setIsConsideredOlderThan15] = useState<
    boolean | null
  >(null);
  const [isConsideredOlderThan13, setIsConsideredOlderThan13] = useState<
    boolean | null
  >(null);

  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  const fetchStatus = async () => {
    try {
      setLoading(true);

      setError(null);

      if (Platform.OS === 'android') {
        const data = await getGooglePlayAgeRangeStatus();

        setAndroidResult(data);
      } else {
        const data = await getAppleDeclaredAgeRangeStatus();

        setAppleResult(data);
      }

      setIsConsideredOlderThan18(await getIsConsideredOlderThan(18));
      setIsConsideredOlderThan15(await getIsConsideredOlderThan(15));
      setIsConsideredOlderThan13(await getIsConsideredOlderThan(13));
    } catch (err: any) {
      console.error('❌ Failed to fetch Age Signals:', err);
      const msg =
        err?.message ??
        err?.nativeStackAndroid ??
        'Unknown error retrieving Play Age Signals';
      setError(msg);
    } finally {
      setLoading(false);
    }
  };

  const title =
    Platform.OS === 'ios'
      ? 'Apple Declared Age Range Demo'
      : 'Google Play Age Signals Demo';

  return (
    <View style={styles.container}>
      <Text style={styles.header}>{title}</Text>

      {loading ? (
        <ActivityIndicator size="large" color="#007aff" />
      ) : error ? (
        <View style={styles.errorBox}>
          <Text style={styles.errorTitle}>Error</Text>
          <Text style={styles.errorText}>{error}</Text>
        </View>
      ) : (
        <ScrollView style={styles.resultBox}>
          {Platform.OS === 'ios' ? (
            <Text style={styles.resultText}>
              Is Eligible: {appleResult ? String(appleResult?.isEligible) : ''}{' '}
              {`\n`}
              Status: {appleResult ? appleResult?.status : ''} {`\n`}
              ParentControls: {appleResult
                ? appleResult?.parentControls
                : ''}{' '}
              {`\n`}
              Lower Bound: {appleResult ? appleResult?.lowerBound : ''} {`\n`}
            </Text>
          ) : (
            <Text style={styles.resultText}>
              User Status:{' '}
              {androidResult && androidResult.userStatus != null
                ? AgeSignalsUserStatusString[androidResult.userStatus]
                : '(no signal)'}
              {'\n'}
              Most Recent Approval Date:{' '}
              {androidResult?.mostRecentApprovalDate ?? '—'} {`\n`}
              Age Lower: {androidResult?.ageLower ?? '—'} {`\n`}
              Age Upper: {androidResult?.ageUpper ?? '—'} {`\n`}
              Error: {androidResult?.error ?? '—'} {`\n`}
            </Text>
          )}

          {isConsideredOlderThan18 ? (
            <Text style={styles.resultText}>
              This is only visible to users older than 18
            </Text>
          ) : (
            <Text style={styles.resultText}>The user is younger than 18</Text>
          )}
          {isConsideredOlderThan15 ? (
            <Text style={styles.resultText}>
              This is only visible to users older than 15
            </Text>
          ) : (
            <Text style={styles.resultText}>The user is younger than 15</Text>
          )}
          {isConsideredOlderThan13 ? (
            <Text style={styles.resultText}>
              This is only visible to users older than 13
            </Text>
          ) : (
            <Text style={styles.resultText}>The user is younger than 13</Text>
          )}
        </ScrollView>
      )}

      <Pressable style={styles.button} onPress={fetchStatus}>
        <Text style={styles.buttonTitle}>Check</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f4f6f8',
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    fontSize: 20,
    fontWeight: '700',
    marginBottom: 20,
  },
  resultBox: {
    maxHeight: 240,
    width: '100%',
    backgroundColor: '#fff',
    borderRadius: 10,
    padding: 12,
    shadowColor: '#000',
    shadowOpacity: 0.1,
    shadowOffset: { width: 0, height: 2 },
    shadowRadius: 6,
    elevation: 2,
  },
  resultText: {
    fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace' }),
    fontSize: 13,
    color: '#333',
  },

  button: {
    backgroundColor: '#2fe5e5',
    borderRadius: 10,
    padding: 12,
    width: '100%',
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 20,
  },

  buttonTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: 'black',
  },

  errorBox: {
    backgroundColor: '#ffe5e5',
    borderRadius: 10,
    padding: 12,
    width: '100%',
  },
  errorTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#b00020',
    marginBottom: 4,
  },
  errorText: {
    color: '#b00020',
    fontSize: 14,
  },
});

🏪 Per-store APIs & normalized results

Each store has its own fetcher; all three Android fetchers resolve to the same normalized AgeSignalsResult, so one code path handles every store. The store's unmodified response is kept in raw:

| Store | Fetcher | | ------------------------------------ | ---------------------------------- | | Google Play (and unknown installers) | getGooglePlayAgeRangeStatus() | | Amazon Appstore | getAmazonAgeRangeStatus() | | Samsung Galaxy Store | getSamsungGalaxyAgeRangeStatus() | | Apple App Store | getAppleDeclaredAgeRangeStatus() |

interface AgeSignalsResult<Raw> {
  store: AppStore;
  userStatus?: AgeSignalsUserStatus; // VERIFIED | DECLARED | SUPERVISED | SUPERVISED_APPROVAL_PENDING | SUPERVISED_APPROVAL_DENIED | CONSENT_NOT_GRANTED | UNKNOWN
  ageLower?: number; // inclusive lower bound of the age range
  ageUpper?: number; // inclusive upper bound; undefined for 18+ users
  mostRecentApprovalDate?: string; // ISO 8601 (YYYY-MM-DD)
  error?: string; // set when the store call failed
  raw: Raw; // the untouched store API response
}

userStatus === undefined means the store returned no signal — the user is not in a region where an age-verification law applies (or, on Google Play, did not consent to share their age). getIsConsideredOlderThan treats both this and failed store calls as "older" so the app keeps working for users the laws don't cover.


🧪 Mocking store responses

Every store's flow can be tested on any device or emulator — see TESTING_ANDROID.md for the full scenario tables:

setGooglePlayMockUser({
  userStatus: PlayAgeSignalsUserStatus.SUPERVISED,
  ageLower: 13,
  ageUpper: 15,
});
setAmazonMockScenario(4); // Amazon test scenario (1–11)
setSamsungMockScenario(4); // Samsung test scenario (1–9)

// Pass undefined to disable a mock and use the real API again.

While a mock is active, detectStore() reports that store, so getIsConsideredOlderThan routes to it — only activate one store's mock at a time.


🔍 Understanding eligibility

Apple's DeclaredAgeRangeResult reports applicability via an isEligible boolean:

  • true: The user is in a region where age verification is legally required.
  • false: The user is not in an applicable region; getIsConsideredOlderThan lets these users view age-gated content (not verified by a lawyer).

On Android, the equivalent signal is a normalized AgeSignalsResult whose userStatus is undefined.


🧩 Supported Platforms

| Platform | Status | | ----------------------------- | -------------------------------------------------------------------- | | Google Play (Android 15+) | ✅ Supported (Age Signals SDK Beta) | | Amazon Appstore | ✅ Supported | | Samsung Galaxy Store | ✅ Supported (Galaxy Store 4.6.03.1+) | | iOS 26.2+ (App Store) | ✅ Supported (tested on 26.5) | | iOS Simulator | ⚠️ Real API not supported | | Emulators | ⚠️ Real APIs not supported — use the mock APIs |


🤝 Contributing

Pull requests welcome!


🪪 License

MIT © Gautham Vijayan


Made with ❤️ and Nitro Modules