age_verification 0.2.0 copy "age_verification: ^0.2.0" to clipboard
age_verification: ^0.2.0 copied to clipboard

A Flutter plugin for querying platform age signals via Google Play Age Signals on Android and Apple DeclaredAgeRange on iOS, for regional age verification compliance.

example/lib/main.dart

import 'dart:io';

import 'package:age_verification/age_verification.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Age Verification Demo',
      theme: ThemeData(useMaterial3: true),
      home: const AgeVerificationPage(),
    );
  }
}

// ---------------------------------------------------------------------------
// Scenarios
// ---------------------------------------------------------------------------
class _Scenario {
  const _Scenario(this.label, this.config);
  final String label;
  final AgeVerificationMockConfig config;
}

final _scenarios = [
  _Scenario(
    'Supervised 13-15',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.supervised,
      ageLower: 13,
      ageUpper: 15,
      installId: 'mock-install-001',
    ),
  ),
  _Scenario(
    'Supervised 16-17',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.supervised,
      ageLower: 16,
      ageUpper: 17,
      installId: 'mock-install-002',
    ),
  ),
  _Scenario(
    'Verified 18+',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.verified,
      ageLower: 18,
    ),
  ),
  _Scenario(
    'Approval Pending',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.supervisedApprovalPending,
      ageLower: 13,
      ageUpper: 15,
      installId: 'mock-install-003',
    ),
  ),
  _Scenario(
    'Approval Denied',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.supervisedApprovalDenied,
      ageLower: 13,
      ageUpper: 15,
      installId: 'mock-install-004',
    ),
  ),
  _Scenario(
    'Declared',
    AgeVerificationMockConfig(
      status: AgeVerificationStatus.declared,
      ageLower: 13,
      ageUpper: 15,
    ),
  ),
  _Scenario(
    'Declined (iOS)',
    AgeVerificationMockConfig(status: AgeVerificationStatus.declined),
  ),
  _Scenario(
    'Unknown',
    AgeVerificationMockConfig(status: AgeVerificationStatus.unknown),
  ),
];

// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
class AgeVerificationPage extends StatefulWidget {
  const AgeVerificationPage({super.key});

  @override
  State<AgeVerificationPage> createState() => _AgeVerificationPageState();
}

class _AgeVerificationPageState extends State<AgeVerificationPage> {
  final _plugin = AgeVerification();

  bool _useMock = false;
  int _selectedScenario = 0;

  bool _initialized = false;
  bool _loading = false;
  String? _error;
  AgeVerificationResult? _result;

  static const _ageGates = [13, 18];

  @override
  void initState() {
    super.initState();
    _initialize();
  }

  Future<void> _initialize({AgeVerificationMockConfig? mockConfig}) async {
    setState(() {
      _loading = true;
      _error = null;
      _result = null;
      _initialized = false;
    });
    try {
      await _plugin.init(mockConfig: mockConfig);
      setState(() => _initialized = true);
    } on PlatformException catch (e) {
      setState(() => _error = 'Init failed [${e.code}]: ${e.message}');
    } finally {
      setState(() => _loading = false);
    }
  }

  Future<void> _verifyAge() async {
    setState(() {
      _loading = true;
      _error = null;
      _result = null;
    });
    try {
      final result = await _plugin.verifyAge(_ageGates);
      setState(() => _result = result);
    } on PlatformException catch (e) {
      setState(() => _error = '[${e.code}] ${e.message}');
    } finally {
      setState(() => _loading = false);
    }
  }

  void _onMockToggled(bool value) {
    setState(() {
      _useMock = value;
      _selectedScenario = 0;
    });
    _initialize(
      mockConfig: value ? _scenarios[_selectedScenario].config : null,
    );
  }

  void _onScenarioSelected(int index) {
    setState(() => _selectedScenario = index);
    _initialize(mockConfig: _scenarios[index].config);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Age Verification'), elevation: 2),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildInfoCard(),
            const SizedBox(height: 12),
            _buildMockCard(),
            if (!_useMock && Platform.isIOS) ...[
              const SizedBox(height: 12),
              _buildIosEntitlementNote(),
            ],
            const SizedBox(height: 16),
            FilledButton.icon(
              onPressed: _initialized && !_loading ? _verifyAge : null,
              icon: const Icon(Icons.verified_user),
              label: Text(_loading ? 'Checking…' : 'Check Age'),
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 14),
              ),
            ),
            const SizedBox(height: 24),
            if (_loading) const Center(child: CircularProgressIndicator()),
            if (_error != null) _ErrorCard(message: _error!),
            if (_result != null) _ResultCard(result: _result!),
          ],
        ),
      ),
    );
  }

  // ---------------------------------------------------------------------------
  // Info card
  // ---------------------------------------------------------------------------
  Widget _buildInfoCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(
                  _initialized ? Icons.check_circle : Icons.hourglass_empty,
                  size: 16,
                  color: _initialized ? Colors.green : Colors.orange,
                ),
                const SizedBox(width: 6),
                Text(
                  _initialized ? 'Initialized' : 'Initializing…',
                  style: TextStyle(
                    color: _initialized ? Colors.green : Colors.orange,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const Spacer(),
                Chip(
                  label: Text(Platform.isAndroid ? 'Android' : 'iOS'),
                  visualDensity: VisualDensity.compact,
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              'Age gates: ${_ageGates.join(', ')} (iOS only)',
              style: Theme.of(context).textTheme.bodySmall,
            ),
            if (_useMock) ...[
              const SizedBox(height: 4),
              Text(
                'Mode: Mock — "${_scenarios[_selectedScenario].label}"',
                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                  color: Colors.blue[700],
                  fontWeight: FontWeight.w600,
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  // ---------------------------------------------------------------------------
  // Mock toggle + scenario chips
  // ---------------------------------------------------------------------------
  Widget _buildMockCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Mock mode',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                      SizedBox(height: 2),
                      Text(
                        'Bypass native API — no entitlement or supervised account needed.',
                        style: TextStyle(fontSize: 12, color: Colors.grey),
                      ),
                    ],
                  ),
                ),
                Switch(value: _useMock, onChanged: _onMockToggled),
              ],
            ),
            if (_useMock) ...[
              const Divider(height: 24),
              Text(
                'Scenario',
                style: Theme.of(
                  context,
                ).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 10),
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: List.generate(_scenarios.length, (i) {
                  final selected = i == _selectedScenario;
                  return FilterChip(
                    label: Text(_scenarios[i].label),
                    selected: selected,
                    onSelected: (_) => _onScenarioSelected(i),
                  );
                }),
              ),
            ],
          ],
        ),
      ),
    );
  }

  // ---------------------------------------------------------------------------
  // iOS entitlement note (real mode only)
  // ---------------------------------------------------------------------------
  Widget _buildIosEntitlementNote() {
    return Card(
      color: Colors.orange[50],
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(Icons.info_outline, color: Colors.orange[700], size: 20),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Entitlement required',
                    style: Theme.of(context).textTheme.titleSmall?.copyWith(
                      color: Colors.orange[800],
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  const Text(
                    'The real DeclaredAgeRange API needs the '
                    'com.apple.developer.declared-age-range entitlement '
                    'and iOS 26.0+. Enable Mock mode above to test without it.',
                    style: TextStyle(fontSize: 12),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Result card
// ---------------------------------------------------------------------------
class _ResultCard extends StatelessWidget {
  const _ResultCard({required this.result});
  final AgeVerificationResult result;

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.green[50],
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(Icons.check_circle_outline, color: Colors.green[700]),
                const SizedBox(width: 8),
                Text(
                  'Result',
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    color: Colors.green[700],
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            _Row('Status', _statusLabel(result.status)),
            if (result.ageLower != null || result.ageUpper != null)
              _Row(
                'Age range',
                '${result.ageLower ?? '?'} - ${result.ageUpper ?? '?'}',
              ),
            if (result.source != null)
              _Row('Source', _sourceLabel(result.source!)),
            if (result.installId != null) _Row('Install ID', result.installId!),
          ],
        ),
      ),
    );
  }

  String _statusLabel(AgeVerificationStatus s) => switch (s) {
    AgeVerificationStatus.verified => 'Verified — meets age requirement',
    AgeVerificationStatus.supervised => 'Supervised — below age threshold',
    AgeVerificationStatus.supervisedApprovalPending =>
      'Supervised — approval pending',
    AgeVerificationStatus.supervisedApprovalDenied =>
      'Supervised — approval denied',
    AgeVerificationStatus.declared => 'Declared — age self-declared via Play',
    AgeVerificationStatus.declined => 'Declined — user chose not to share',
    AgeVerificationStatus.unknown => 'Unknown — no signal available',
  };

  String _sourceLabel(AgeDeclarationSource s) => switch (s) {
    AgeDeclarationSource.selfDeclared => 'Self declared',
    AgeDeclarationSource.guardianDeclared => 'Guardian declared',
  };
}

// ---------------------------------------------------------------------------
// Error card
// ---------------------------------------------------------------------------
class _ErrorCard extends StatelessWidget {
  const _ErrorCard({required this.message});
  final String message;

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Theme.of(context).colorScheme.errorContainer,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(
              Icons.error_outline,
              color: Theme.of(context).colorScheme.onErrorContainer,
            ),
            const SizedBox(width: 8),
            Expanded(
              child: Text(
                message,
                style: TextStyle(
                  color: Theme.of(context).colorScheme.onErrorContainer,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Row helper
// ---------------------------------------------------------------------------
class _Row extends StatelessWidget {
  const _Row(this.label, this.value);
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 90,
            child: Text(
              label,
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
}
0
likes
0
points
257
downloads

Publisher

verified publishergtirkha.com

Weekly Downloads

A Flutter plugin for querying platform age signals via Google Play Age Signals on Android and Apple DeclaredAgeRange on iOS, for regional age verification compliance.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on age_verification

Packages that implement age_verification