appattest_flutter 0.1.0 copy "appattest_flutter: ^0.1.0" to clipboard
appattest_flutter: ^0.1.0 copied to clipboard

PlatformiOS

Flutter bridge for AppAttest — App-Attest-gated secret delivery for iOS.

example/lib/main.dart

// AppAttestExample — Flutter reference app (v4.12).
//
// Demonstrates the v4.12 ergonomics:
//   - AppAttest.start() at app boot.
//   - AppAttest.stateStream drives splash / ready / failure UI.
//   - AppAttest.allSecrets() reads the populated dict.
//
// Before running:
//   1. Set your Apple Team in Xcode (ios/Runner.xcworkspace).
//   2. Change the bundle identifier if the default isn't available.
//   3. Add the App Attest capability via "+ Capability → App Attest".
//   4. Ask api to register your (teamId, bundleId) under the development
//      environment with at least one secret.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:appattest_flutter/appattest_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // v7.3: SDK is bucket-blind + base URL hardcoded to edge.appattest.dev.
  // No setup required beyond start().
  await AppAttest.start();
  runApp(const AppAttestApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AppAttest · Flutter',
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: const Color(0xFF3B82F6),
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  AppAttestState _state = AppAttestState(name: AppAttestStateName.initializing);
  Map<String, String> _secrets = {};
  bool _revealAll = false;
  StreamSubscription<AppAttestState>? _stateSub;

  @override
  void initState() {
    super.initState();
    _bootstrap();
    _stateSub = AppAttest.stateStream.listen((s) {
      if (!mounted) return;
      setState(() => _state = s);
      if (s.name == AppAttestStateName.ready) _refreshSecrets();
    });
  }

  Future<void> _bootstrap() async {
    final s = await AppAttest.getState();
    if (!mounted) return;
    setState(() => _state = s);
    if (s.name == AppAttestStateName.ready) _refreshSecrets();
  }

  Future<void> _refreshSecrets() async {
    try {
      final m = await AppAttest.allSecrets();
      if (!mounted) return;
      setState(() => _secrets = m);
    } catch (_) {/* ignore */}
  }

  @override
  void dispose() {
    _stateSub?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF8FAFC),
      appBar: AppBar(
        title: const Text('AppAttest · Flutter',
            style: TextStyle(fontWeight: FontWeight.w700)),
        centerTitle: true,
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _StateCard(state: _state),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => AppAttest.retry(),
                    child: const Text('Retry'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton(
                    onPressed: _secrets.isEmpty
                        ? null
                        : () => setState(() => _revealAll = !_revealAll),
                    child: Text(_revealAll ? 'Hide secrets' : 'Show secrets'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton(
                    onPressed: () async {
                      await AppAttest.reset();
                      if (!mounted) return;
                      setState(() => _secrets = {});
                    },
                    style: OutlinedButton.styleFrom(
                        foregroundColor: const Color(0xFFEF4444)),
                    child: const Text('Reset'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            // Force fresh sync via invalidateBundle() — wipes the cached
            // bundle and re-syncs without losing attestation. Exercises
            // the bridge path added 2026-05-25 (RN/Cap/Flutter all wired
            // through to AppAttestClient.invalidateBundle()).
            OutlinedButton(
              onPressed: _state.name == AppAttestStateName.ready
                  ? () async {
                      await AppAttest.invalidateBundle();
                      if (!mounted) return;
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('Force fresh sync — invalidateBundle() called'),
                          duration: Duration(seconds: 2),
                        ),
                      );
                    }
                  : null,
              style: OutlinedButton.styleFrom(
                  foregroundColor: const Color(0xFF3B82F6)),
              child: const Text('Force fresh sync (uses 1 credit)'),
            ),
            if (_state.name == AppAttestStateName.ready) ...[
              const SizedBox(height: 16),
              _SecretsPanel(secrets: _secrets, revealAll: _revealAll),
            ],
          ],
        ),
      ),
    );
  }
}

class _StateCard extends StatelessWidget {
  const _StateCard({required this.state});
  final AppAttestState state;

  @override
  Widget build(BuildContext context) {
    final (color, label, detail) = switch (state.name) {
      AppAttestStateName.initializing =>
        (const Color(0xFF94A3B8), 'Initializing', 'Hydrating from Keychain'),
      AppAttestStateName.attesting =>
        (const Color(0xFF3B82F6), 'Attesting', 'Generating attestation with Apple'),
      AppAttestStateName.syncing =>
        (const Color(0xFF3B82F6), 'Syncing', 'Fetching secrets from api'),
      AppAttestStateName.ready =>
        (const Color(0xFF22C55E), 'Ready', 'Secrets cached.'),
      AppAttestStateName.subscriptionRequired => (
        const Color(0xFFF59E0B),
        'Subscription required',
        state.error?.message ?? 'Subscribe this project in the AppAttest dashboard'
      ),
      AppAttestStateName.creditsRequired => (
        const Color(0xFFF59E0B),
        'Credits required',
        state.error?.message ?? 'Top up the project balance'
      ),
      AppAttestStateName.unavailable => (
        const Color(0xFFEF4444),
        'Unavailable',
        state.error?.message ?? 'Service temporarily unavailable'
      ),
    };
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: color, width: 2),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(label,
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
          const SizedBox(height: 4),
          Text(detail,
              style: const TextStyle(fontSize: 13, color: Color(0xFF475569))),
          if (state.error?.actionUrl != null) ...[
            const SizedBox(height: 4),
            SelectableText(
              'Open: ${state.error!.actionUrl!}',
              style:
                  const TextStyle(fontSize: 11, fontFamily: 'Menlo', color: Color(0xFF64748B)),
            ),
          ],
        ],
      ),
    );
  }
}

class _SecretsPanel extends StatelessWidget {
  const _SecretsPanel({required this.secrets, required this.revealAll});
  final Map<String, String> secrets;
  final bool revealAll;

  @override
  Widget build(BuildContext context) {
    final names = secrets.keys.toList()..sort();
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text('Secrets (${names.length})',
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
        const SizedBox(height: 4),
        if (names.isEmpty)
          const Padding(
            padding: EdgeInsets.all(12),
            child: Text('No secrets registered for this env.',
                style: TextStyle(fontSize: 12, color: Color(0xFF64748B))),
          )
        else
          ...names.map((n) => _SecretRow(
                name: n,
                value: secrets[n]!,
                revealed: revealAll,
              )),
      ],
    );
  }
}

class _SecretRow extends StatefulWidget {
  const _SecretRow({required this.name, required this.value, required this.revealed});
  final String name;
  final String value;
  final bool revealed;

  @override
  State<_SecretRow> createState() => _SecretRowState();
}

class _SecretRowState extends State<_SecretRow> {
  bool _localReveal = false;

  @override
  Widget build(BuildContext context) {
    final visible = widget.revealed || _localReveal;
    final masked = '•' * widget.value.length.clamp(0, 20);
    return Container(
      padding: const EdgeInsets.all(12),
      margin: const EdgeInsets.symmetric(vertical: 4),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: const Color(0xFFE2E8F0)),
      ),
      child: Row(
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(widget.name,
                    style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700)),
                const SizedBox(height: 2),
                SelectableText(
                  visible ? widget.value : masked,
                  style: const TextStyle(
                      fontSize: 12, fontFamily: 'Menlo', color: Color(0xFF64748B)),
                ),
              ],
            ),
          ),
          OutlinedButton(
            onPressed: () => setState(() => _localReveal = !_localReveal),
            child: Text(visible ? 'Hide' : 'Show',
                style: const TextStyle(fontSize: 12)),
          ),
        ],
      ),
    );
  }
}
0
likes
150
points
86
downloads

Documentation

API reference

Publisher

verified publisherappattest.dev

Weekly Downloads

Flutter bridge for AppAttest — App-Attest-gated secret delivery for iOS.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on appattest_flutter

Packages that implement appattest_flutter