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

Native device-signer plugin for the Crossmint Flutter SDK — hardware-backed key generation and signing via Secure Enclave (iOS) and Android Keystore.

example/lib/main.dart

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

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

abstract class DeviceSignerExampleBackend {
  Future<bool> isAvailable();

  Future<String?> getDeviceName();

  Future<String> generateKey({String? address});

  Future<String?> getKey(String address);

  Future<bool> hasKey(String publicKeyBase64);

  Future<void> mapAddressToKey(String address, String publicKeyBase64);

  Future<Map<String, Object?>> signMessage({
    required String address,
    required String message,
  });

  Future<void> deleteKey(String address);

  Future<void> deletePendingKey(String publicKeyBase64);
}

class PluginDeviceSignerExampleBackend implements DeviceSignerExampleBackend {
  const PluginDeviceSignerExampleBackend();

  static const MethodChannel _channel = MethodChannel(
    'crossmint_device_signer',
  );
  static const CrossmintDeviceSigner _plugin = CrossmintDeviceSigner();

  @override
  Future<bool> isAvailable() async {
    final bool? value = await _channel.invokeMethod<bool>('isAvailable');
    return value ?? false;
  }

  @override
  Future<String?> getDeviceName() {
    return _plugin.getDeviceName();
  }

  @override
  Future<String> generateKey({String? address}) {
    return _plugin.generateKey(address: address);
  }

  @override
  Future<String?> getKey(String address) {
    return _plugin.getKey(address);
  }

  @override
  Future<bool> hasKey(String publicKeyBase64) {
    return _plugin.hasKey(publicKeyBase64);
  }

  @override
  Future<void> mapAddressToKey(String address, String publicKeyBase64) {
    return _plugin.mapAddressToKey(address, publicKeyBase64);
  }

  @override
  Future<Map<String, Object?>> signMessage({
    required String address,
    required String message,
  }) {
    return _plugin.signMessage(address: address, message: message);
  }

  @override
  Future<void> deleteKey(String address) {
    return _plugin.deleteKey(address);
  }

  @override
  Future<void> deletePendingKey(String publicKeyBase64) {
    return _channel.invokeMethod<void>('deletePendingKey', <String, Object?>{
      'publicKeyBase64': publicKeyBase64,
    });
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, this.backend = const PluginDeviceSignerExampleBackend()});

  final DeviceSignerExampleBackend backend;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A6C7A)),
        useMaterial3: true,
      ),
      home: DeviceSignerHarnessScreen(backend: backend),
    );
  }
}

class DeviceSignerHarnessScreen extends StatefulWidget {
  const DeviceSignerHarnessScreen({super.key, required this.backend});

  final DeviceSignerExampleBackend backend;

  @override
  State<DeviceSignerHarnessScreen> createState() =>
      _DeviceSignerHarnessScreenState();
}

class _DeviceSignerHarnessScreenState extends State<DeviceSignerHarnessScreen> {
  final TextEditingController _addressController =
      TextEditingController(text: '0x1234567890123456789012345678901234567890');
  final TextEditingController _publicKeyController = TextEditingController();
  final TextEditingController _messageController = TextEditingController(
    text: 'aGVsbG8=',
  );
  final List<String> _events = <String>[];

  String _deviceName = 'Unknown';
  String _availability = 'Unknown';
  String _lastLookup = 'n/a';
  String _lastHasKey = 'n/a';
  String _lastSignature = 'n/a';
  bool _busy = false;

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

  Future<void> _bootstrap() async {
    try {
      final String? deviceName = await widget.backend.getDeviceName();
      if (mounted) {
        setState(() {
          _deviceName = deviceName?.trim().isNotEmpty == true
              ? deviceName!
              : 'Unknown device';
        });
      }
    } on PlatformException catch (error) {
      _appendEvent('device name error: ${error.code}');
    }

    try {
      final bool available = await widget.backend.isAvailable();
      if (mounted) {
        setState(() {
          _availability = available ? 'available' : 'unavailable';
        });
      }
    } on PlatformException catch (error) {
      _appendEvent('availability error: ${error.code}');
    }
  }

  @override
  void dispose() {
    _addressController.dispose();
    _publicKeyController.dispose();
    _messageController.dispose();
    super.dispose();
  }

  Future<void> _checkAvailability() async {
    await _run('availability', () async {
      final bool available = await widget.backend.isAvailable();
      setState(() {
        _availability = available ? 'available' : 'unavailable';
      });
    });
  }

  Future<void> _generatePendingKey() async {
    await _run('generate pending key', () async {
      final String publicKey = await widget.backend.generateKey();
      setState(() {
        _publicKeyController.text = publicKey;
      });
      _appendEvent('generated pending key: $publicKey');
    });
  }

  Future<void> _mapKeyToAddress() async {
    await _run('map key to address', () async {
      final String address = _trimmed(_addressController.text);
      final String publicKey = _trimmed(_publicKeyController.text);
      await widget.backend.mapAddressToKey(address, publicKey);
      _appendEvent('mapped $publicKey to $address');
    });
  }

  Future<void> _getKeyByAddress() async {
    await _run('get key by address', () async {
      final String address = _trimmed(_addressController.text);
      final String? publicKey = await widget.backend.getKey(address);
      setState(() {
        _lastLookup = publicKey ?? 'null';
        if (publicKey != null && publicKey.isNotEmpty) {
          _publicKeyController.text = publicKey;
        }
      });
      _appendEvent('getKey($address) -> ${publicKey ?? 'null'}');
    });
  }

  Future<void> _hasKey() async {
    await _run('has key', () async {
      final String publicKey = _trimmed(_publicKeyController.text);
      final bool hasKey = await widget.backend.hasKey(publicKey);
      setState(() {
        _lastHasKey = hasKey.toString();
      });
      _appendEvent('hasKey($publicKey) -> $hasKey');
    });
  }

  Future<void> _signMessage() async {
    await _run('sign base64 message', () async {
      final String address = _trimmed(_addressController.text);
      final String message = _trimmed(_messageController.text);
      final Map<String, Object?> signature = await widget.backend.signMessage(
        address: address,
        message: message,
      );
      setState(() {
        _lastSignature = signature.toString();
      });
      _appendEvent('signMessage($address) -> $signature');
    });
  }

  Future<void> _deleteKey() async {
    await _run('delete key', () async {
      final String address = _trimmed(_addressController.text);
      await widget.backend.deleteKey(address);
      _appendEvent('deleted key for $address');
    });
  }

  Future<void> _deletePendingKey() async {
    await _run('delete pending key', () async {
      final String publicKey = _trimmed(_publicKeyController.text);
      await widget.backend.deletePendingKey(publicKey);
      _appendEvent('deleted pending key $publicKey');
    });
  }

  Future<void> _run(String label, Future<void> Function() action) async {
    if (_busy) {
      return;
    }
    setState(() {
      _busy = true;
    });
    try {
      await action();
    } on PlatformException catch (error) {
      _appendEvent('$label error: ${error.code} ${error.message ?? ''}'.trim());
    } catch (error) {
      _appendEvent('$label error: $error');
    } finally {
      if (mounted) {
        setState(() {
          _busy = false;
        });
      }
    }
  }

  void _appendEvent(String event) {
    if (!mounted) {
      return;
    }
    setState(() {
      _events.insert(0, event);
      if (_events.length > 20) {
        _events.removeLast();
      }
    });
  }

  String _trimmed(String value) {
    final String trimmed = value.trim();
    if (trimmed.isEmpty) {
      throw const FormatException('Required value is empty.');
    }
    return trimmed;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Crossmint Device Signer Spike')),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              _HeaderCard(
                deviceName: _deviceName,
                availability: _availability,
                busy: _busy,
              ),
              const SizedBox(height: 16),
              _FieldCard(
                title: 'Address',
                child: TextField(
                  controller: _addressController,
                  decoration: const InputDecoration(
                    labelText: 'Wallet address',
                    helperText: 'Used for map/get/delete actions.',
                  ),
                ),
              ),
              const SizedBox(height: 12),
              _FieldCard(
                title: 'Public Key',
                child: TextField(
                  controller: _publicKeyController,
                  decoration: const InputDecoration(
                    labelText: 'Base64 public key',
                    helperText: 'Generated key is reused for map / has / delete.',
                  ),
                ),
              ),
              const SizedBox(height: 12),
              _FieldCard(
                title: 'Message',
                child: TextField(
                  controller: _messageController,
                  decoration: const InputDecoration(
                    labelText: 'Base64 message',
                    helperText: 'Plugin expects base64-encoded bytes.',
                  ),
                ),
              ),
              const SizedBox(height: 16),
              _ActionGrid(
                actions: <_HarnessAction>[
                  _HarnessAction(
                    label: 'Availability',
                    onPressed: _checkAvailability,
                  ),
                  _HarnessAction(
                    label: 'Generate pending key',
                    onPressed: _generatePendingKey,
                  ),
                  _HarnessAction(
                    label: 'Map key to address',
                    onPressed: _mapKeyToAddress,
                  ),
                  _HarnessAction(
                    label: 'Get key by address',
                    onPressed: _getKeyByAddress,
                  ),
                  _HarnessAction(
                    label: 'Has key',
                    onPressed: _hasKey,
                  ),
                  _HarnessAction(
                    label: 'Sign base64 message',
                    onPressed: _signMessage,
                  ),
                  _HarnessAction(
                    label: 'Delete key',
                    onPressed: _deleteKey,
                  ),
                  _HarnessAction(
                    label: 'Delete pending key',
                    onPressed: _deletePendingKey,
                  ),
                ],
              ),
              const SizedBox(height: 16),
              _StatusCard(
                lastLookup: _lastLookup,
                lastHasKey: _lastHasKey,
                lastSignature: _lastSignature,
                events: _events,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _HeaderCard extends StatelessWidget {
  const _HeaderCard({
    required this.deviceName,
    required this.availability,
    required this.busy,
  });

  final String deviceName;
  final String availability;
  final bool busy;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'Device: $deviceName',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Text('Signer availability: $availability'),
            const SizedBox(height: 8),
            Text(busy ? 'Busy' : 'Idle'),
          ],
        ),
      ),
    );
  }
}

class _FieldCard extends StatelessWidget {
  const _FieldCard({required this.title, required this.child});

  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(title, style: Theme.of(context).textTheme.titleSmall),
            const SizedBox(height: 12),
            child,
          ],
        ),
      ),
    );
  }
}

class _ActionGrid extends StatelessWidget {
  const _ActionGrid({required this.actions});

  final List<_HarnessAction> actions;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Wrap(
          spacing: 12,
          runSpacing: 12,
          children: actions
              .map(
                (_HarnessAction action) => FilledButton.tonal(
                  onPressed: action.onPressed,
                  child: Text(action.label),
                ),
              )
              .toList(growable: false),
        ),
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({
    required this.lastLookup,
    required this.lastHasKey,
    required this.lastSignature,
    required this.events,
  });

  final String lastLookup;
  final String lastHasKey;
  final String lastSignature;
  final List<String> events;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Last lookup: $lastLookup'),
            const SizedBox(height: 4),
            Text('Last hasKey: $lastHasKey'),
            const SizedBox(height: 4),
            Text('Last signature: $lastSignature'),
            const SizedBox(height: 16),
            Text('Event log', style: Theme.of(context).textTheme.titleSmall),
            const SizedBox(height: 8),
            if (events.isEmpty)
              const Text('No events yet.')
            else
              ...events.map((String event) => Padding(
                    padding: const EdgeInsets.only(bottom: 4),
                    child: Text('- $event'),
                  )),
          ],
        ),
      ),
    );
  }
}

class _HarnessAction {
  const _HarnessAction({required this.label, required this.onPressed});

  final String label;
  final Future<void> Function() onPressed;
}
0
likes
140
points
49
downloads

Documentation

API reference

Publisher

verified publishercrossmint.com

Weekly Downloads

Native device-signer plugin for the Crossmint Flutter SDK — hardware-backed key generation and signing via Secure Enclave (iOS) and Android Keystore.

Homepage
Repository (GitHub)
View/report issues

Topics

#crossmint #security

License

Apache-2.0 (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on crossmint_device_signer

Packages that implement crossmint_device_signer