solana_kit_mobile_wallet_adapter 0.2.1 copy "solana_kit_mobile_wallet_adapter: ^0.2.1" to clipboard
solana_kit_mobile_wallet_adapter: ^0.2.1 copied to clipboard

Flutter plugin for the Solana Mobile Wallet Adapter protocol. Enables dApps to communicate with wallet apps for transaction signing on Android. iOS compiles but is a no-op.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:solana_kit_mobile_wallet_adapter/solana_kit_mobile_wallet_adapter.dart';
import 'package:solana_kit_mobile_wallet_adapter_protocol/solana_kit_mobile_wallet_adapter_protocol.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const ManualMwaExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Solana Kit MWA Manual Test',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF161627)),
        useMaterial3: true,
      ),
      home: const ManualMwaHomePage(),
    );
  }
}

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

  @override
  State<ManualMwaHomePage> createState() => _ManualMwaHomePageState();
}

class _ManualMwaHomePageState extends State<ManualMwaHomePage> {
  final MwaClientHostApi _hostApi = MwaClientHostApi();
  final TextEditingController _messageController = TextEditingController(
    text: 'Hello from solana_kit_mobile_wallet_adapter example',
  );

  final List<String> _logs = <String>[];

  final bool _isMwaSupported = isMwaSupported();

  bool _isBusy = false;
  bool? _walletEndpointAvailable;
  AuthorizationResult? _authorization;
  WalletCapabilities? _capabilities;
  String? _lastSignedPayload;

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

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

  Future<void> _runOperation(
    String label,
    Future<void> Function() operation,
  ) async {
    if (_isBusy) {
      return;
    }

    setState(() {
      _isBusy = true;
    });

    _appendLog('$label started.');

    try {
      await operation();
      _appendLog('$label succeeded.');
    } on Object catch (error) {
      _appendLog('$label failed: $error');
      _showSnackBar('$label failed. Check log for details.');
    } finally {
      if (mounted) {
        setState(() {
          _isBusy = false;
        });
      }
    }
  }

  Future<void> _refreshWalletEndpointStatus() async {
    if (!_isMwaSupported) {
      setState(() {
        _walletEndpointAvailable = false;
      });
      return;
    }

    final isAvailable = await _hostApi.isWalletEndpointAvailable();

    if (!mounted) {
      return;
    }

    setState(() {
      _walletEndpointAvailable = isAvailable;
    });

    _appendLog(
      isAvailable
          ? 'Wallet endpoint detected on device.'
          : 'No wallet endpoint detected. Install a mock or compatible wallet.',
    );
  }

  Future<void> _authorize() async {
    await _runOperation('Authorize', () async {
      final auth = await transact(
        (wallet) => wallet.authorize(
          chain: 'solana:devnet',
          features: const ['solana:signMessages'],
        ),
      );

      if (!mounted) {
        return;
      }

      setState(() {
        _authorization = auth;
      });

      final accountCount = auth.accounts.length;
      final accountPreview = accountCount > 0
          ? (auth.accounts.first.displayAddress ?? auth.accounts.first.address)
          : 'No accounts returned';

      _appendLog(
        'Authorized $accountCount account(s). First account: $accountPreview',
      );
    });
  }

  Future<void> _loadCapabilities() async {
    final auth = _authorization;
    if (auth == null) {
      _appendLog('Get capabilities skipped: authorize first.');
      _showSnackBar('Authorize first.');
      return;
    }

    await _runOperation('Get capabilities', () async {
      final capabilities = await transact((wallet) async {
        final refreshedAuth = await wallet.reauthorize(
          authToken: auth.authToken,
        );
        final fetchedCapabilities = await wallet.getCapabilities();
        return (refreshedAuth, fetchedCapabilities);
      });

      if (!mounted) {
        return;
      }

      final (refreshedAuth, fetchedCapabilities) = capabilities;

      setState(() {
        _authorization = refreshedAuth;
        _capabilities = fetchedCapabilities;
      });

      final features = fetchedCapabilities.features?.join(', ') ?? 'none';
      _appendLog('Capabilities loaded. Features: $features');
    });
  }

  Future<void> _signMessage() async {
    final auth = _authorization;
    if (auth == null || auth.accounts.isEmpty) {
      _appendLog('Sign message skipped: authorize first.');
      _showSnackBar('Authorize first.');
      return;
    }

    final message = _messageController.text.trim();
    if (message.isEmpty) {
      _appendLog('Sign message skipped: message is empty.');
      _showSnackBar('Enter a message to sign.');
      return;
    }

    await _runOperation('Sign message', () async {
      final encodedMessage = base64Encode(utf8.encode(message));

      final signed = await transact((wallet) async {
        final refreshedAuth = await wallet.reauthorize(
          authToken: auth.authToken,
        );

        if (refreshedAuth.accounts.isEmpty) {
          throw StateError('Wallet returned no accounts after reauthorize.');
        }

        final signedPayloads = await wallet.signMessages(
          addresses: <String>[refreshedAuth.accounts.first.address],
          payloads: <String>[encodedMessage],
        );

        return (refreshedAuth, signedPayloads);
      });

      final (refreshedAuth, signedPayloads) = signed;

      if (signedPayloads.isEmpty) {
        throw StateError('Wallet returned no signed payloads.');
      }

      if (!mounted) {
        return;
      }

      setState(() {
        _authorization = refreshedAuth;
        _lastSignedPayload = signedPayloads.first;
      });

      _appendLog(
        'Message signed. Returned ${signedPayloads.length} signed payload(s).',
      );
    });
  }

  Future<void> _deauthorize() async {
    final auth = _authorization;
    if (auth == null) {
      _appendLog('Deauthorize skipped: no active authorization.');
      _showSnackBar('No active authorization.');
      return;
    }

    await _runOperation('Deauthorize', () async {
      await transact((wallet) => wallet.deauthorize(authToken: auth.authToken));

      if (!mounted) {
        return;
      }

      setState(() {
        _authorization = null;
        _capabilities = null;
        _lastSignedPayload = null;
      });

      _appendLog('Authorization revoked.');
    });
  }

  void _appendLog(String message) {
    if (!mounted) {
      return;
    }

    final now = DateTime.now();
    final timestamp =
        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';

    setState(() {
      _logs.insert(0, '[$timestamp] $message');
    });
  }

  void _showSnackBar(String message) {
    if (!mounted) {
      return;
    }

    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  String _walletEndpointLabel() {
    final isAvailable = _walletEndpointAvailable;
    if (isAvailable == null) {
      return 'Checking...';
    }
    return isAvailable ? 'Available' : 'Not found';
  }

  String _truncate(String value, {int max = 72}) {
    if (value.length <= max) {
      return value;
    }

    return '${value.substring(0, max)}...';
  }

  @override
  Widget build(BuildContext context) {
    final auth = _authorization;
    final activeAccount = auth != null && auth.accounts.isNotEmpty
        ? (auth.accounts.first.displayAddress ?? auth.accounts.first.address)
        : null;

    return Scaffold(
      appBar: AppBar(title: const Text('Solana Kit MWA Manual Test')),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: <Widget>[
            Text(
              'Use this app to manually test authorize, capabilities, signing, and deauthorize flows on Android wallets supporting Mobile Wallet Adapter.',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 16),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      'Status',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    _StatusLine(
                      label: 'MWA supported',
                      value: _isMwaSupported ? 'Yes' : 'No',
                    ),
                    _StatusLine(
                      label: 'Wallet endpoint',
                      value: _walletEndpointLabel(),
                    ),
                    _StatusLine(
                      label: 'Authorized account',
                      value: activeAccount ?? 'Not authorized',
                    ),
                    _StatusLine(label: 'Busy', value: _isBusy ? 'Yes' : 'No'),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                ElevatedButton(
                  onPressed: _isBusy
                      ? null
                      : () {
                          _runOperation(
                            'Refresh wallet endpoint',
                            _refreshWalletEndpointStatus,
                          );
                        },
                  child: const Text('Refresh Wallet Status'),
                ),
                ElevatedButton(
                  onPressed: !_isBusy && _isMwaSupported ? _authorize : null,
                  child: const Text('Authorize'),
                ),
                ElevatedButton(
                  onPressed: !_isBusy && auth != null
                      ? _loadCapabilities
                      : null,
                  child: const Text('Get Capabilities'),
                ),
                ElevatedButton(
                  onPressed: !_isBusy && auth != null ? _signMessage : null,
                  child: const Text('Sign Message'),
                ),
                OutlinedButton(
                  onPressed: !_isBusy && auth != null ? _deauthorize : null,
                  child: const Text('Deauthorize'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _messageController,
              minLines: 2,
              maxLines: 4,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Message to sign',
                hintText: 'Enter any text payload',
              ),
            ),
            const SizedBox(height: 16),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      'Capabilities',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    _StatusLine(
                      label: 'Features',
                      value:
                          _capabilities?.features?.join(', ') ?? 'Not loaded',
                    ),
                    _StatusLine(
                      label: 'Max messages/request',
                      value:
                          _capabilities?.maxMessagesPerRequest?.toString() ??
                          'Not loaded',
                    ),
                    _StatusLine(
                      label: 'Max tx/request',
                      value:
                          _capabilities?.maxTransactionsPerRequest
                              ?.toString() ??
                          'Not loaded',
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      'Last signed payload (base64)',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    SelectableText(
                      _lastSignedPayload == null
                          ? 'No payload signed yet.'
                          : _truncate(_lastSignedPayload!),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      'Operation log',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    SelectableText(
                      _logs.isEmpty ? 'No operations yet.' : _logs.join('\n'),
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusLine extends StatelessWidget {
  const _StatusLine({required this.label, required this.value});

  final String label;
  final String value;

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

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for the Solana Mobile Wallet Adapter protocol. Enables dApps to communicate with wallet apps for transaction signing on Android. iOS compiles but is a no-op.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, solana_kit_addresses, solana_kit_errors, solana_kit_keys, solana_kit_mobile_wallet_adapter_protocol, solana_kit_transactions, web_socket_channel

More

Packages that depend on solana_kit_mobile_wallet_adapter

Packages that implement solana_kit_mobile_wallet_adapter