Skip to main content

flutter_keychain_fix 0.0.3 copy "flutter_keychain_fix: ^0.0.3" to clipboard
flutter_keychain_fix: ^0.0.3 copied to clipboard

Flutter secure storage via Keychain (iOS) and Keystore (Android).

example/lib/main.dart

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

void main() => runApp(const ExampleApp());

/// Demo shell.
///
/// Each button on the home screen maps to exactly one method of
/// [FlutterKeychainFix], and every call reports what came back, so the example doubles
/// as a way to try each method and see its result.
class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_keychain_fix',
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      home: const KeychainPage(),
    );
  }
}

/// The key this page reads and writes. A real app would use keys such as `auth_token`;
/// one fixed key keeps the demo readable.
const _demoKey = 'demo_key';

/// One line in the activity list.
class _Entry {
  const _Entry(this.text, {this.isError = false});

  final String text;
  final bool isError;
}

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

  @override
  State<KeychainPage> createState() => _KeychainPageState();
}

class _KeychainPageState extends State<KeychainPage> {
  final _valueController = TextEditingController(text: 'hello world');
  final _labelController = TextEditingController();

  final List<_Entry> _entries = [];
  String? _stored;
  bool _busy = false;

  @override
  void initState() {
    super.initState();
    // Show what is already there, so the page is useful before anything is tapped.
    WidgetsBinding.instance.addPostFrameCallback((_) => _read(silent: true));
  }

  @override
  void dispose() {
    _valueController.dispose();
    _labelController.dispose();
    super.dispose();
  }

  /// Runs [body], then prepends one line describing the outcome.
  ///
  /// Platform failures surface as [PlatformException] and an unregistered plugin as
  /// [MissingPluginException]; both are reported in the list instead of being swallowed,
  /// because on Android a mis-typed storage identifier fails exactly this quietly.
  ///
  /// Returns the entry it added so a caller that wants to retract its own line can do so
  /// by identity. Removing by index would drop whichever line happened to be first by
  /// then, which is not necessarily the one this call wrote.
  Future<_Entry?> _perform(
    String operation,
    Future<String> Function() body,
  ) async {
    setState(() => _busy = true);

    String message;
    var isError = false;
    try {
      message = await body();
    } on PlatformException catch (e) {
      message = '${e.code}: ${e.message ?? 'no message'}';
      isError = true;
    } on MissingPluginException {
      message = 'the plugin is not registered on this platform';
      isError = true;
    } catch (e) {
      message = e.toString();
      isError = true;
    }

    if (!mounted) return null;
    final entry = _Entry('$operation → $message', isError: isError);
    setState(() {
      _busy = false;
      _entries.insert(0, entry);
    });
    return entry;
  }

  Future<void> _save() async {
    final value = _valueController.text;
    await _perform('put', () async {
      await FlutterKeychainFix.put(key: _demoKey, value: value);
      await _syncStoredValue();
      return 'stored "$value" under "$_demoKey"';
    });
  }

  Future<void> _read({bool silent = false}) async {
    final entry = await _perform('get', () async {
      await _syncStoredValue();
      return _stored == null
          ? 'nothing stored under "$_demoKey"'
          : 'read "${_stored!}"';
    });
    // The opening read runs before the user has touched anything. It still refreshes the
    // card above, but logging it would make the activity list look as though something
    // had already failed.
    if (silent && entry != null && mounted) {
      setState(() => _entries.remove(entry));
    }
  }

  Future<void> _delete() async {
    await _perform('remove', () async {
      await FlutterKeychainFix.remove(key: _demoKey);
      await _syncStoredValue();
      return 'deleted "$_demoKey"';
    });
  }

  Future<void> _clearAll() async {
    await _perform('clear', () async {
      await FlutterKeychainFix.clear();
      await _syncStoredValue();
      return 'deleted every entry, encryption key kept';
    });
  }

  /// Applies the iOS keychain label. Android ignores it, which is worth seeing: the call
  /// succeeds and changes nothing there.
  Future<void> _configure() async {
    final label = _labelController.text;
    await _perform('configure', () async {
      await FlutterKeychainFix.configure(label: label.isEmpty ? null : label);
      return label.isEmpty
          ? 'reset to the default iOS configuration'
          : 'label set to "$label" for items written from now on';
    });
  }

  Future<void> _syncStoredValue() async {
    final value = await FlutterKeychainFix.get(key: _demoKey);
    if (mounted) setState(() => _stored = value);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(title: const Text('flutter_keychain_fix')),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            Text('Secure store', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text(
              'Values are encrypted by the iOS Keychain and the Android Keystore, then '
              'read back by key.',
              style: theme.textTheme.bodySmall,
            ),
            const SizedBox(height: 16),

            TextField(
              controller: _valueController,
              decoration: const InputDecoration(
                labelText: 'Value to store',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),

            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton(
                  onPressed: _busy ? null : _save,
                  child: const Text('Save'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _read,
                  child: const Text('Read'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _delete,
                  child: const Text('Delete key'),
                ),
                OutlinedButton(
                  onPressed: _busy ? null : _clearAll,
                  child: const Text('Clear all'),
                ),
              ],
            ),
            const SizedBox(height: 16),

            _StoredValueCard(value: _stored),

            const SizedBox(height: 24),
            const Divider(),
            const SizedBox(height: 8),

            Text('iOS configuration', style: theme.textTheme.titleMedium),
            const SizedBox(height: 4),
            Text(
              'A label controls how an item appears in iOS Passwords. Apply it before '
              'saving if you want one. Android has no equivalent and ignores this.',
              style: theme.textTheme.bodySmall,
            ),
            const SizedBox(height: 12),

            TextField(
              controller: _labelController,
              decoration: const InputDecoration(
                labelText: 'Keychain label (optional)',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            OutlinedButton(
              onPressed: _busy ? null : _configure,
              child: const Text('Apply configuration'),
            ),

            const SizedBox(height: 24),
            const Divider(),
            const SizedBox(height: 8),

            Text('Activity', style: theme.textTheme.titleMedium),
            const SizedBox(height: 8),
            if (_entries.isEmpty)
              Text(
                'Nothing yet — tap a button above.',
                style: theme.textTheme.bodySmall,
              )
            else
              for (final entry in _entries)
                Padding(
                  padding: const EdgeInsets.only(bottom: 6),
                  child: Text(
                    entry.text,
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: entry.isError ? theme.colorScheme.error : null,
                    ),
                  ),
                ),
          ],
        ),
      ),
    );
  }
}

class _StoredValueCard extends StatelessWidget {
  const _StoredValueCard({required this.value});

  final String? value;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      margin: EdgeInsets.zero,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Currently stored under "$_demoKey"',
              style: theme.textTheme.labelMedium,
            ),
            const SizedBox(height: 4),
            Text(
              value ?? '(nothing)',
              style: theme.textTheme.bodyLarge?.copyWith(
                fontStyle: value == null ? FontStyle.italic : null,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
160
points
102
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter secure storage via Keychain (iOS) and Keystore (Android).

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on flutter_keychain_fix

Packages that implement flutter_keychain_fix