portal_flutter 0.1.0-alpha.3 copy "portal_flutter: ^0.1.0-alpha.3" to clipboard
portal_flutter: ^0.1.0-alpha.3 copied to clipboard

Flutter SDK for Portal - Stablecoin wallet infrastructure

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:portal_flutter/portal_flutter.dart';

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

// ==================== Environment Configuration ====================

enum AppEnv { staging, production }

class AppSettings {
  static AppEnv currentEnv = AppEnv.production;
  static bool shouldBackupWithPortal = false;
  static bool isAccountAbstraction = false;

  static String get apiHost {
    switch (currentEnv) {
      case AppEnv.staging:
        return 'api.portalhq.dev';
      case AppEnv.production:
        return 'api.portalhq.io';
    }
  }

  static String get mpcHost {
    switch (currentEnv) {
      case AppEnv.staging:
        return 'mpc.portalhq.dev';
      case AppEnv.production:
        return 'mpc.portalhq.io';
    }
  }

  static String get custodianServerUrl {
    switch (currentEnv) {
      case AppEnv.staging:
        if (shouldBackupWithPortal) {
          return 'https://staging-portalex-backup-with-portal.onrender.com';
        } else {
          return 'https://staging-portalex-mpc-service.onrender.com';
        }
      case AppEnv.production:
        if (shouldBackupWithPortal) {
          return 'https://prod-portalex-backup-with-portal.onrender.com';
        } else {
          return 'https://portalex-mpc.portalhq.io';
        }
    }
  }

  static String get envName {
    switch (currentEnv) {
      case AppEnv.staging:
        return 'Staging';
      case AppEnv.production:
        return 'Production';
    }
  }

  static String get passkeyRelyingPartyId {
    switch (currentEnv) {
      case AppEnv.staging:
        return 'portalhq.dev';
      case AppEnv.production:
        return 'portalhq.io';
    }
  }

  static String get passkeyWebAuthnHost {
    switch (currentEnv) {
      case AppEnv.staging:
        return 'backup.portalhq.dev';
      case AppEnv.production:
        return 'backup.web.portalhq.io';
    }
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Portal Flutter SDK',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AuthScreen(),
    );
  }
}

// ==================== User Session ====================

class UserSession {
  static final UserSession _instance = UserSession._internal();
  factory UserSession() => _instance;
  UserSession._internal();

  UserData? user;

  bool get isLoggedIn => user != null;
  String get clientApiKey => user?.clientApiKey ?? '';
  String get username => user?.username ?? '';
}

class UserData {
  final String clientApiKey;
  final String clientId;
  final int exchangeUserId;
  final String username;

  UserData({
    required this.clientApiKey,
    required this.clientId,
    required this.exchangeUserId,
    required this.username,
  });

  factory UserData.fromJson(Map<String, dynamic> json) {
    return UserData(
      clientApiKey: json['clientApiKey'] as String? ?? '',
      clientId: json['clientId'] as String? ?? '',
      exchangeUserId: json['exchangeUserId'] as int? ?? 0,
      username: json['username'] as String? ?? '',
    );
  }
}

// ==================== Portal API Service ====================

class PortalApiService {
  String get _baseUrl => AppSettings.custodianServerUrl;

  Future<UserData> signUp(String username, {bool isAccountAbstracted = false}) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/mobile/signup'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'isAccountAbstracted': isAccountAbstracted,
      }),
    );

    if (response.statusCode == 200 || response.statusCode == 201) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return UserData.fromJson(json);
    } else {
      throw Exception('Signup failed: ${response.body}');
    }
  }

  Future<UserData> signIn(String username) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/mobile/login'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'username': username}),
    );

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return UserData.fromJson(json);
    } else {
      throw Exception('Login failed: ${response.body}');
    }
  }

  /// Store cipherText with custodian (for non-backup-with-Portal mode)
  Future<void> storeCipherText({
    required int exchangeUserId,
    required String cipherText,
    required String backupMethod,
  }) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/mobile/$exchangeUserId/cipher-text'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'cipherText': cipherText,
        'backupMethod': backupMethod,
      }),
    );

    if (response.statusCode != 200 && response.statusCode != 201) {
      throw Exception('Failed to store cipher text: ${response.body}');
    }
  }

  /// Retrieve cipherText from custodian (for non-backup-with-Portal mode)
  Future<String?> getCipherText({
    required int exchangeUserId,
    required String backupMethod,
  }) async {
    final uri = Uri.parse('$_baseUrl/mobile/$exchangeUserId/cipher-text/fetch')
        .replace(queryParameters: {'backupMethod': backupMethod});
    final response = await http.get(
      uri,
      headers: {'Content-Type': 'application/json'},
    );

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return json['cipherText'] as String?;
    } else if (response.statusCode == 404) {
      return null; // No cipher text found
    } else {
      throw Exception('Failed to get cipher text: ${response.body}');
    }
  }

  /// Enable eject for a wallet (required before calling ejectPrivateKeys with backup-with-Portal)
  Future<String> enableEject({
    required int exchangeUserId,
    required String walletId,
  }) async {
    final response = await http.patch(
      Uri.parse('$_baseUrl/mobile/$exchangeUserId/enable-eject'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'walletId': walletId,
      }),
    );

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return json['ejectableUntil'] as String;
    } else {
      throw Exception('Failed to enable eject: ${response.body}');
    }
  }
}

// ==================== Auth Screen ====================

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

  @override
  State<AuthScreen> createState() => _AuthScreenState();
}

class _AuthScreenState extends State<AuthScreen> {
  final _usernameController = TextEditingController();
  final _apiService = PortalApiService();
  bool _isLoading = false;
  String? _error;

  Future<void> _signUp() async {
    final username = _usernameController.text.trim();
    if (username.isEmpty) {
      setState(() => _error = 'Please enter a username');
      return;
    }

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final userData = await _apiService.signUp(
        username,
        isAccountAbstracted: AppSettings.isAccountAbstraction,
      );
      UserSession().user = userData;
      _navigateToMain();
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _signIn() async {
    final username = _usernameController.text.trim();
    if (username.isEmpty) {
      setState(() => _error = 'Please enter a username');
      return;
    }

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final userData = await _apiService.signIn(username);
      UserSession().user = userData;
      _navigateToMain();
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      setState(() => _isLoading = false);
    }
  }

  void _navigateToMain() {
    Navigator.of(context).pushReplacement(
      MaterialPageRoute(builder: (_) => const MainScreen()),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Portal Flutter SDK'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const Icon(
                Icons.account_balance_wallet,
                size: 80,
                color: Colors.deepPurple,
              ),
              const SizedBox(height: 32),
              const Text(
                'Welcome to Portal',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 8),
              const Text(
                'Sign in or create an account to get started',
                style: TextStyle(fontSize: 14, color: Colors.grey),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 24),
              // Environment Toggle
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Column(
                  children: [
                    const Text(
                      'Environment',
                      style: TextStyle(fontSize: 14),
                    ),
                    const SizedBox(height: 8),
                    SegmentedButton<AppEnv>(
                      segments: const [
                        ButtonSegment<AppEnv>(
                          value: AppEnv.staging,
                          label: Text('Staging'),
                        ),
                        ButtonSegment<AppEnv>(
                          value: AppEnv.production,
                          label: Text('Prod'),
                        ),
                      ],
                      selected: {AppSettings.currentEnv},
                      onSelectionChanged: _isLoading
                          ? null
                          : (Set<AppEnv> selection) {
                              setState(() {
                                AppSettings.currentEnv = selection.first;
                              });
                            },
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              // Backup with Portal Toggle
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text(
                      'Backup with Portal',
                      style: TextStyle(fontSize: 14),
                    ),
                    Switch(
                      value: AppSettings.shouldBackupWithPortal,
                      onChanged: _isLoading
                          ? null
                          : (bool value) {
                              setState(() {
                                AppSettings.shouldBackupWithPortal = value;
                              });
                            },
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              // Account Abstraction Toggle
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Account Abstraction',
                          style: TextStyle(fontSize: 14),
                        ),
                        Switch(
                          value: AppSettings.isAccountAbstraction,
                          onChanged: _isLoading
                              ? null
                              : (bool value) {
                                  setState(() {
                                    AppSettings.isAccountAbstraction = value;
                                  });
                                },
                        ),
                      ],
                    ),
                    const Text(
                      'Enables gas sponsorship for transactions',
                      style: TextStyle(fontSize: 11, color: Colors.grey),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 24),
              TextField(
                controller: _usernameController,
                decoration: const InputDecoration(
                  labelText: 'Username',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.person),
                ),
                enabled: !_isLoading,
                textInputAction: TextInputAction.done,
                onSubmitted: (_) => _signIn(),
              ),
              if (_error != null) ...[
                const SizedBox(height: 16),
                Text(
                  _error!,
                  style: const TextStyle(color: Colors.red),
                  textAlign: TextAlign.center,
                ),
              ],
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _isLoading ? null : _signIn,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: _isLoading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('Sign In'),
              ),
              const SizedBox(height: 12),
              OutlinedButton(
                onPressed: _isLoading ? null : _signUp,
                style: OutlinedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: const Text('Create Account'),
              ),
              const SizedBox(height: 24),
              Text(
                'Server: ${AppSettings.custodianServerUrl}',
                style: const TextStyle(fontSize: 10, color: Colors.grey),
                textAlign: TextAlign.center,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ==================== Main Screen ====================

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

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  final _portal = Portal();
  final _apiService = PortalApiService();
  final _passwordController = TextEditingController();
  String _status = 'Ready to initialize Portal';
  String? _ethereumAddress;
  String? _solanaAddress;
  bool _isInitialized = false;
  bool _hasWallet = false;
  String? _lastBackupCipherText; // Store cipher text from backup for eject

  // Chain IDs
  static const String _sepoliaChainId = 'eip155:11155111';
  static const String _solanaDevnetChainId = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';

  // Test recipient addresses
  static const String _testRecipient =
      '0x4cd042bba0da4b3f37ea36e8a2737dce2ed70db7';
  // Solana Devnet test recipient (Portal's devnet faucet address)
  static const String _testSolanaRecipient =
      'GPsPXxoQA51aTJJkNHtFDFYui5hN5UxcFPnheJEHa5Du';

  void _log(String message) {
    debugPrint('[PortalExample] $message');
    if (mounted) {
      setState(() => _status = message);
    }
  }

  // ==================== Initialization ====================

  Future<void> _initializePortal() async {
    final apiKey = UserSession().clientApiKey;
    if (apiKey.isEmpty) {
      _log('No API key available. Please sign in first.');
      return;
    }

    _log('Initializing Portal (${AppSettings.envName})...');
    try {
      await _portal.initialize(
        apiKey: apiKey,
        rpcConfig: {
          _sepoliaChainId: 'https://rpc.sepolia.org',
          _solanaDevnetChainId: 'https://api.devnet.solana.com',
        },
        autoApprove: true,
        apiHost: AppSettings.apiHost,
        mpcHost: AppSettings.mpcHost,
      );

      _isInitialized = true;

      // Check if wallet already exists on device
      final addressResult = await _portal.getAddresses();
      if (addressResult.hasWallet) {
        _ethereumAddress = addressResult.addresses.ethereum;
        _solanaAddress = addressResult.addresses.solana;
        _hasWallet = true;
        _log(
            'Portal initialized (${AppSettings.envName})!\nExisting wallet found:\nETH: ${_truncate(_ethereumAddress!)}\nSOL: ${_truncate(_solanaAddress!)}');
      } else {
        final recoverable = await _portal.isPasswordRecoverAvailable();
        _log(
            'Portal initialized (${AppSettings.envName})!\nNo wallet on device. Recovery available: $recoverable');
      }
    } on PortalException catch (e) {
      _log('Initialization failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Initialization failed: $e');
    }
  }

  // ==================== Wallet Operations ====================

  Future<void> _createWallet() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Creating wallet...');
    try {
      final addresses = await _portal.createWallet();
      _ethereumAddress = addresses.ethereum;
      _solanaAddress = addresses.solana;
      _hasWallet = true;
      _log(
          'Wallet created!\nETH: ${_truncate(_ethereumAddress!)}\nSOL: ${_truncate(_solanaAddress!)}');
    } on PortalException catch (e) {
      _log('Create wallet failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Create wallet failed: $e');
    }
  }

  // ==================== Backup & Recovery ====================

  Future<void> _backupWallet() async {
    final password = _passwordController.text.trim();
    if (password.isEmpty) {
      _log('Please enter a password');
      return;
    }

    if (!_isInitialized || !_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Backing up wallet with password...');
    try {
      final backupResponse = await _portal.backupWallet(
        method: PortalBackupMethod.password,
        password: password,
      );

      // If not using backup with Portal, store cipherText with custodian
      if (!AppSettings.shouldBackupWithPortal) {
        _log('Storing cipher text with custodian...');
        await _apiService.storeCipherText(
          exchangeUserId: UserSession().user!.exchangeUserId,
          cipherText: backupResponse.cipherText,
          backupMethod: 'PASSWORD',
        );
      }

      _lastBackupCipherText = backupResponse.cipherText;
      _log('Wallet backed up successfully!');
    } on PortalException catch (e) {
      _log('Backup failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Backup failed: $e');
    }
  }

  Future<void> _backupWithICloud() async {
    if (!_isInitialized || !_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Backing up wallet with iCloud...');
    try {
      await _portal.configureICloudStorage();
      await _portal.backupWallet(method: PortalBackupMethod.iCloud);
      _log('Wallet backed up to iCloud successfully!');
    } on PortalException catch (e) {
      _log('iCloud backup failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('iCloud backup failed: $e');
    }
  }

  Future<void> _backupWithPasskey() async {
    if (!_isInitialized || !_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Backing up wallet with Passkey...');
    try {
      // Configure passkey storage first
      await _portal.configurePasskeyStorage(
        relyingPartyId: AppSettings.passkeyRelyingPartyId,
        relyingPartyOrigins: [AppSettings.passkeyWebAuthnHost],
        webAuthnHost: AppSettings.passkeyWebAuthnHost,
      );
      final backupResponse =
          await _portal.backupWallet(method: PortalBackupMethod.passkey);

      // If not using backup with Portal, store cipherText with custodian
      if (!AppSettings.shouldBackupWithPortal) {
        _log('Storing cipher text with custodian...');
        await _apiService.storeCipherText(
          exchangeUserId: UserSession().user!.exchangeUserId,
          cipherText: backupResponse.cipherText,
          backupMethod: 'PASSKEY',
        );
        _lastBackupCipherText = backupResponse.cipherText;
      }

      _log('Wallet backed up with Passkey successfully!');
    } on PortalException catch (e) {
      _log('Passkey backup failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Passkey backup failed: $e');
    }
  }

  Future<void> _recoverWallet() async {
    final password = _passwordController.text.trim();
    if (password.isEmpty) {
      _log('Please enter a password');
      return;
    }

    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Recovering wallet...');
    try {
      String? cipherText;

      // If not using backup with Portal, retrieve cipherText from custodian
      if (!AppSettings.shouldBackupWithPortal) {
        _log('Retrieving cipher text from custodian...');
        cipherText = await _apiService.getCipherText(
          exchangeUserId: UserSession().user!.exchangeUserId,
          backupMethod: 'PASSWORD',
        );
        if (cipherText == null) {
          _log('No backup found. Please backup your wallet first.');
          return;
        }
      }

      final addresses = await _portal.recoverWallet(
        method: PortalBackupMethod.password,
        password: password,
        cipherText: cipherText,
      );
      _ethereumAddress = addresses.ethereum;
      _solanaAddress = addresses.solana;
      _hasWallet = true;
      _log(
          'Wallet recovered!\nETH: ${_truncate(_ethereumAddress!)}\nSOL: ${_truncate(_solanaAddress!)}');
    } on PortalException catch (e) {
      _log('Recovery failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Recovery failed: $e');
    }
  }

  Future<void> _recoverWithICloud() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Recovering wallet from iCloud...');
    try {
      await _portal.configureICloudStorage();
      final addresses = await _portal.recoverWallet(
        method: PortalBackupMethod.iCloud,
      );
      _ethereumAddress = addresses.ethereum;
      _solanaAddress = addresses.solana;
      _hasWallet = true;
      _log(
          'Wallet recovered from iCloud!\nETH: ${_truncate(_ethereumAddress!)}\nSOL: ${_truncate(_solanaAddress!)}');
    } on PortalException catch (e) {
      _log('iCloud recovery failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('iCloud recovery failed: $e');
    }
  }

  Future<void> _recoverWithPasskey() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Recovering wallet with Passkey...');
    try {
      await _portal.configurePasskeyStorage(
        relyingPartyId: AppSettings.passkeyRelyingPartyId,
        relyingPartyOrigins: [AppSettings.passkeyWebAuthnHost],
        webAuthnHost: AppSettings.passkeyWebAuthnHost,
      );

      // If not using backup with Portal, retrieve cipherText from custodian
      String? cipherText;
      if (!AppSettings.shouldBackupWithPortal) {
        _log('Retrieving cipher text from custodian...');
        cipherText = await _apiService.getCipherText(
          exchangeUserId: UserSession().user!.exchangeUserId,
          backupMethod: 'PASSKEY',
        );
        if (cipherText == null) {
          _log('No cipher text found. Please backup first.');
          return;
        }
      }

      final addresses = await _portal.recoverWallet(
        method: PortalBackupMethod.passkey,
        cipherText: cipherText,
      );
      _ethereumAddress = addresses.ethereum;
      _solanaAddress = addresses.solana;
      _hasWallet = true;
      _log(
          'Wallet recovered with Passkey!\nETH: ${_truncate(_ethereumAddress!)}\nSOL: ${_truncate(_solanaAddress!)}');
    } on PortalException catch (e) {
      _log('Passkey recovery failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Passkey recovery failed: $e');
    }
  }

  Future<void> _ejectPrivateKeys() async {
    final password = _passwordController.text.trim();
    if (password.isEmpty) {
      _log('Please enter a password');
      return;
    }

    if (!_isInitialized || !_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Ejecting private keys...');
    try {
      String? cipherText;

      if (AppSettings.shouldBackupWithPortal) {
        // Backup-with-Portal mode: Portal stores the cipherText
        // We need to enable eject first via custodian
        _log('Enabling eject via custodian...');

        // Get client info to find wallet IDs
        final clientInfo = await _portal.getClient();

        // Enable eject for each wallet
        for (final wallet in clientInfo.wallets) {
          if (wallet != null) {
            final ejectableUntil = await _apiService.enableEject(
              exchangeUserId: UserSession().user!.exchangeUserId,
              walletId: wallet.id,
            );
            _log('Wallet ${wallet.id} ejectable until: $ejectableUntil');
          }
        }
        // cipherText stays null - Portal has it stored
      } else {
        // Non-backup-with-Portal mode: Custodian stores the cipherText
        // Retrieve it from custodian
        _log('Retrieving cipher text from custodian...');
        cipherText = await _apiService.getCipherText(
          exchangeUserId: UserSession().user!.exchangeUserId,
          backupMethod: 'PASSWORD',
        );
        if (cipherText == null) {
          _log('No backup found. Please backup your wallet first.');
          return;
        }
      }

      final keys = await _portal.ejectPrivateKeys(
        method: PortalBackupMethod.password,
        password: password,
        cipherText: cipherText,
      );
      final keyCount = keys.length;
      _log('Private keys ejected!\n$keyCount keys retrieved.');

      // Show dialog with copyable keys
      if (mounted) {
        _showEjectedKeysDialog(keys);
      }
    } on PortalException catch (e) {
      _log('Eject failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Eject failed: $e');
    }
  }

  void _showEjectedKeysDialog(Map<String, String> keys) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Row(
          children: [
            Icon(Icons.warning, color: Colors.red),
            SizedBox(width: 8),
            Text('Private Keys'),
          ],
        ),
        content: SizedBox(
          width: double.maxFinite,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                'WARNING: Store these securely! Anyone with these keys has full control of your wallet.',
                style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 16),
              ...keys.entries.map((entry) => Padding(
                padding: const EdgeInsets.only(bottom: 12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      entry.key.toUpperCase(),
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 4),
                    Container(
                      padding: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: Row(
                        children: [
                          Expanded(
                            child: SelectableText(
                              entry.value,
                              style: const TextStyle(
                                fontFamily: 'monospace',
                                fontSize: 12,
                              ),
                            ),
                          ),
                          IconButton(
                            icon: const Icon(Icons.copy, size: 20),
                            onPressed: () {
                              Clipboard.setData(ClipboardData(text: entry.value));
                              ScaffoldMessenger.of(context).showSnackBar(
                                SnackBar(
                                  content: Text('${entry.key} key copied!'),
                                  duration: const Duration(seconds: 2),
                                ),
                              );
                            },
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              )),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  Future<void> _deleteKeychain() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Deleting keychain data...');
    try {
      await _portal.deleteKeychain();
      _hasWallet = false;
      _ethereumAddress = null;
      _solanaAddress = null;
      _log('Keychain deleted successfully!\nWallet data removed from device.');
    } on PortalException catch (e) {
      _log('Delete keychain failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Delete keychain failed: $e');
    }
  }

  Future<void> _checkWalletStatus() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Checking wallet status...');
    try {
      final exists = await _portal.doesWalletExist();
      final onDevice = await _portal.isWalletOnDevice();
      final backedUp = await _portal.isWalletBackedUp();
      final recoverable = await _portal.isWalletRecoverable();

      _log('''Wallet Status:
- Exists: $exists
- On Device: $onDevice
- Backed Up: $backedUp
- Recoverable: $recoverable''');
    } on PortalException catch (e) {
      _log('Status check failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Status check failed: $e');
    }
  }

  Future<void> _getAssets() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Getting assets on Sepolia...');
    try {
      final assets = await _portal.getAssets(_sepoliaChainId);
      final nativeBalances = assets.nativeBalance.where((a) => a != null).toList();
      final tokenBalances = assets.tokenBalances.where((a) => a != null).toList();

      var result = 'Assets on Sepolia:\n';
      result += 'Native Balance:\n';
      for (final asset in nativeBalances) {
        result += '  ${asset!.symbol}: ${asset.balance}\n';
      }
      if (tokenBalances.isNotEmpty) {
        result += 'Token Balances:\n';
        for (final asset in tokenBalances) {
          result += '  ${asset!.symbol}: ${asset.balance}\n';
        }
      } else {
        result += 'No token balances';
      }
      _log(result);
    } on PortalException catch (e) {
      _log('Get assets failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Get assets failed: $e');
    }
  }

  Future<void> _getAvailableRecoveryMethods() async {
    if (!_isInitialized) {
      _log('Please initialize Portal first');
      return;
    }

    _log('Getting available recovery methods...');
    try {
      final methods = await _portal.availableRecoveryMethods();
      if (methods.isEmpty) {
        _log('No recovery methods available');
      } else {
        _log('Available recovery methods:\n${methods.map((m) => '  - $m').join('\n')}');
      }
    } on PortalException catch (e) {
      _log('Get recovery methods failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Get recovery methods failed: $e');
    }
  }

  // ==================== Signing ====================

  Future<void> _signMessage() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Signing message...');
    try {
      const message = 'Hello from Portal Flutter SDK!';
      final signature = await _portal.signMessage(
        chainId: _sepoliaChainId,
        message: message,
      );
      _log('Message signed!\nSignature: ${_truncate(signature)}');
    } on PortalException catch (e) {
      _log('Sign message failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Sign message failed: $e');
    }
  }

  Future<void> _signTypedData() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Signing typed data (EIP-712)...');
    try {
      const typedData = '''
{
  "types": {
    "EIP712Domain": [
      {"name": "name", "type": "string"},
      {"name": "version", "type": "string"},
      {"name": "chainId", "type": "uint256"}
    ],
    "Message": [
      {"name": "content", "type": "string"}
    ]
  },
  "primaryType": "Message",
  "domain": {
    "name": "Portal Example",
    "version": "1",
    "chainId": 11155111
  },
  "message": {
    "content": "Hello from Portal!"
  }
}
''';

      final signature = await _portal.signTypedData(
        chainId: _sepoliaChainId,
        typedData: typedData,
      );
      _log('Typed data signed!\nSignature: ${_truncate(signature)}');
    } on PortalException catch (e) {
      _log('Sign typed data failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Sign typed data failed: $e');
    }
  }

  Future<void> _ethSign() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Signing with eth_sign...');
    try {
      // eth_sign takes address and data hash
      final messageHash = '0x${List.generate(64, (_) => 'a').join()}'; // 32 bytes hex
      final result = await _portal.request(
        chainId: _sepoliaChainId,
        method: 'eth_sign',
        params: [_ethereumAddress!, messageHash],
      );
      _log('eth_sign result:\n${_truncate(result.result ?? 'null')}');
    } on PortalException catch (e) {
      _log('eth_sign failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('eth_sign failed: $e');
    }
  }

  Future<void> _getBalance() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Getting ETH balance...');
    try {
      final result = await _portal.request(
        chainId: _sepoliaChainId,
        method: 'eth_getBalance',
        params: [_ethereumAddress!, 'latest'],
      );

      var balanceHex = result.result ?? '0x0';

      // Handle case where result might be a JSON string or wrapped response
      if (balanceHex.contains('"result"')) {
        // Try to extract the result from JSON
        final match = RegExp(r'"result":\s*"?(0x[0-9a-fA-F]+)"?').firstMatch(balanceHex);
        if (match != null) {
          balanceHex = match.group(1) ?? '0x0';
        }
      }

      // Ensure we have a valid hex string
      if (!balanceHex.startsWith('0x')) {
        _log('Balance response: $balanceHex');
        return;
      }

      final balanceWei = int.parse(balanceHex.substring(2), radix: 16);
      final balanceEth = balanceWei / 1e18;
      _log('Balance: ${balanceEth.toStringAsFixed(6)} ETH\n($balanceWei wei)');
    } on PortalException catch (e) {
      _log('Get balance failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Get balance failed: $e');
    }
  }

  Future<void> _signSolanaMessage() async {
    if (!_hasWallet || _solanaAddress == null) {
      _log('Please create a wallet first');
      return;
    }

    _log('Signing Solana message...');
    try {
      const message = 'Hello from Portal Flutter SDK on Solana!';
      final result = await _portal.request(
        chainId: _solanaDevnetChainId,
        method: 'sol_signMessage',
        params: [message],
      );
      // Check for errors in the response
      if (result.error != null) {
        _log('Solana sign failed: ${result.error}');
        return;
      }
      _log('Solana message signed!\nSignature: ${_truncate(result.result ?? 'null')}');
    } on PortalException catch (e) {
      _log('Solana sign failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Solana sign failed: $e');
    }
  }

  Future<void> _sendSol() async {
    if (!_hasWallet || _solanaAddress == null) {
      _log('Please create a wallet first');
      return;
    }

    _log('Sending 0.01 SOL on Devnet...');
    try {
      final txHash = await _portal.sendAsset(
        chainId: _solanaDevnetChainId,
        to: _testSolanaRecipient,
        token: 'SOL',
        amount: '0.01',
      );
      _log('SOL sent!\nTransaction: ${_truncate(txHash)}');
    } on PortalException catch (e) {
      _log('Send SOL failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Send SOL failed: $e');
    }
  }

  Future<void> _rawSign() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Signing raw message...');
    try {
      // Raw sign takes a hex-encoded message (without personal_sign prefix)
      final messageHash = '0x${List.generate(64, (_) => 'b').join()}'; // 32 bytes hex
      final signature = await _portal.rawSign(
        chainId: _sepoliaChainId,
        message: messageHash,
        signatureApprovalMemo: 'Raw sign test',
      );
      _log('Raw message signed!\nSignature: ${_truncate(signature)}');
    } on PortalException catch (e) {
      _log('Raw sign failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Raw sign failed: $e');
    }
  }

  // ==================== Transactions ====================

  Future<void> _sendTransaction() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Sending transaction (1 wei)...');
    try {
      final txHash = await _portal.sendTransaction(
        chainId: _sepoliaChainId,
        to: _testRecipient,
        value: '0x1', // 1 wei
      );
      _log('Transaction sent!\nHash: ${_truncate(txHash)}');
    } on PortalException catch (e) {
      _log('Send transaction failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Send transaction failed: $e');
    }
  }

  Future<void> _fundTestnet() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Requesting testnet ETH on Sepolia...');
    try {
      final result = await _portal.receiveTestnetAsset(
        chainId: _sepoliaChainId,
        amount: '0.01',
        token: 'ETH',
      );
      if (result.success) {
        _log('Testnet funds received!\nTx: ${_truncate(result.transactionHash ?? '')}');
      } else {
        _log('Fund request failed: ${result.error ?? 'Unknown error'}');
      }
    } on PortalException catch (e) {
      _log('Fund testnet failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Fund testnet failed: $e');
    }
  }

  Future<void> _sendAssetWithGasSponsorship() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Sending asset with gas sponsorship...');
    try {
      final txHash = await _portal.sendAsset(
        chainId: _sepoliaChainId,
        to: _testRecipient,
        token: 'ETH',
        amount: '0.0001',
        sponsorGas: true,
      );
      _log('Asset sent with gas sponsorship!\nHash: ${_truncate(txHash)}');
    } on PortalException catch (e) {
      _log('Send asset failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Send asset failed: $e');
    }
  }

  Future<void> _getTransactions() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Getting transactions on Sepolia...');
    try {
      final transactions = await _portal.getTransactions(
        _sepoliaChainId,
        limit: 5,
        order: PortalTransactionOrder.desc,
      );

      if (transactions.isEmpty) {
        _log('No transactions found');
        return;
      }

      var result = 'Recent Transactions (${transactions.length}):\n';
      for (final tx in transactions) {
        result += '  ${_truncate(tx.hash ?? 'unknown', 8)}: ${tx.value ?? '0'} wei\n';
        result += '    From: ${_truncate(tx.from ?? 'unknown', 6)}\n';
        result += '    To: ${_truncate(tx.to ?? 'unknown', 6)}\n';
      }
      _log(result);
    } on PortalException catch (e) {
      _log('Get transactions failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Get transactions failed: $e');
    }
  }

  Future<void> _getNftAssets() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Getting NFT assets on Sepolia...');
    try {
      final nfts = await _portal.getNftAssets(_sepoliaChainId);

      if (nfts.isEmpty) {
        _log('No NFTs found');
        return;
      }

      var result = 'NFT Assets (${nfts.length}):\n';
      for (final nft in nfts) {
        result += '  ${nft.name ?? 'Unnamed'} (#${nft.tokenId ?? 'unknown'})\n';
        result += '    Contract: ${_truncate(nft.contractAddress ?? 'unknown', 8)}\n';
        result += '    Type: ${nft.tokenType ?? 'unknown'}\n';
      }
      _log(result);
    } on PortalException catch (e) {
      _log('Get NFTs failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Get NFTs failed: $e');
    }
  }

  Future<void> _evaluateTransaction() async {
    if (!_hasWallet) {
      _log('Please create a wallet first');
      return;
    }

    _log('Evaluating transaction security...');
    try {
      final result = await _portal.evaluateTransaction(
        chainId: _sepoliaChainId,
        to: _testRecipient,
        value: '0x1',
        operationType: PortalEvaluateTransactionOperationType.all,
      );

      _log('''Transaction Evaluation:
- Result: ${result.result ?? 'unknown'}
- Status: ${result.status ?? 'unknown'}
- Classification: ${result.classification ?? 'unknown'}
- Reason: ${result.reason ?? 'none'}
- Description: ${result.description ?? 'none'}''');
    } on PortalException catch (e) {
      _log('Evaluate transaction failed: [${e.code}] ${e.message}');
    } catch (e) {
      _log('Evaluate transaction failed: $e');
    }
  }

  // ==================== Helpers ====================

  String _truncate(String s, [int length = 16]) {
    if (s.length <= length * 2) return s;
    return '${s.substring(0, length)}...${s.substring(s.length - length)}';
  }

  void _copyAddress() {
    if (_ethereumAddress != null) {
      Clipboard.setData(ClipboardData(text: _ethereumAddress!));
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Address copied to clipboard')),
      );
    }
  }

  void _signOut() {
    UserSession().user = null;
    Navigator.of(context).pushReplacement(
      MaterialPageRoute(builder: (_) => const AuthScreen()),
    );
  }

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

  // ==================== UI ====================

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Welcome, ${UserSession().username}'),
          actions: [
            IconButton(
              icon: const Icon(Icons.logout),
              onPressed: _signOut,
              tooltip: 'Sign Out',
            ),
          ],
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(text: 'Setup'),
              Tab(text: 'Backup'),
              Tab(text: 'Signing'),
              Tab(text: 'Transactions'),
            ],
          ),
        ),
        body: Column(
          children: [
            // Status bar
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(12),
              color: Colors.grey[200],
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Status: $_status',
                    style: const TextStyle(fontSize: 12),
                  ),
                  if (_ethereumAddress != null) ...[
                    const SizedBox(height: 4),
                    GestureDetector(
                      onTap: _copyAddress,
                      child: Text(
                        'ETH: ${_truncate(_ethereumAddress!, 10)}',
                        style: const TextStyle(
                          fontSize: 11,
                          color: Colors.blue,
                        ),
                      ),
                    ),
                  ],
                  if (_solanaAddress != null) ...[
                    const SizedBox(height: 2),
                    Text(
                      'SOL: ${_truncate(_solanaAddress!, 10)}',
                      style: const TextStyle(
                        fontSize: 11,
                        color: Colors.purple,
                      ),
                    ),
                  ],
                ],
              ),
            ),
            // Tab content
            Expanded(
              child: TabBarView(
                children: [
                  _buildSetupTab(),
                  _buildBackupTab(),
                  _buildSigningTab(),
                  _buildTransactionsTab(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSetupTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // User Info Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Session Info',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  Text('Username: ${UserSession().username}'),
                  Text('Environment: ${AppSettings.envName}'),
                  Text('API Key: ${_truncate(UserSession().clientApiKey, 8)}'),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          const Text(
            'Initialization',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _initializePortal,
            child: const Text('Initialize Portal'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Wallet',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _createWallet : null,
            child: const Text('Create Wallet'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _checkWalletStatus : null,
            child: const Text('Check Wallet Status'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _getBalance : null,
            child: const Text('Get ETH Balance'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _getAssets : null,
            child: const Text('Get Assets'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _getAvailableRecoveryMethods : null,
            child: const Text('Available Recovery Methods'),
          ),
        ],
      ),
    );
  }

  Widget _buildBackupTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Password field for password-based operations
          TextField(
            controller: _passwordController,
            decoration: const InputDecoration(
              labelText: 'Password',
              hintText: 'For password backup/recovery/eject',
              border: OutlineInputBorder(),
            ),
            obscureText: true,
          ),
          const SizedBox(height: 24),
          const Text(
            'Password Backup',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _backupWallet : null,
            child: const Text('Backup with Password'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _recoverWallet : null,
            child: const Text('Recover with Password'),
          ),
          const SizedBox(height: 24),
          const Text(
            'iCloud Backup (iOS)',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _backupWithICloud : null,
            child: const Text('Backup to iCloud'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _recoverWithICloud : null,
            child: const Text('Recover from iCloud'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Passkey Backup',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Uses WebAuthn for secure backup (iOS 16+)',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _backupWithPasskey : null,
            child: const Text('Backup with Passkey'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _recoverWithPasskey : null,
            child: const Text('Recover with Passkey'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Private Key Export',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            '⚠️ WARNING: Exports full private keys. Use with caution!',
            style: TextStyle(fontSize: 12, color: Colors.red),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _ejectPrivateKeys : null,
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red[100]),
            child: const Text('Eject Private Keys'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Delete Keychain',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Removes all local wallet data from device',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _isInitialized ? _deleteKeychain : null,
            style: ElevatedButton.styleFrom(backgroundColor: Colors.orange[100]),
            child: const Text('Delete Keychain'),
          ),
        ],
      ),
    );
  }

  Widget _buildSigningTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Ethereum Signing',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          const Text(
            'personal_sign',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Sign a personal message with prefix',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _signMessage : null,
            child: const Text('Personal Sign'),
          ),
          const SizedBox(height: 16),
          const Text(
            'eth_sign',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Sign raw data hash (32 bytes)',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _ethSign : null,
            child: const Text('Eth Sign'),
          ),
          const SizedBox(height: 16),
          const Text(
            'eth_signTypedData_v4',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Sign EIP-712 structured data',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _signTypedData : null,
            child: const Text('Sign Typed Data (v4)'),
          ),
          const SizedBox(height: 16),
          const Text(
            'raw_sign',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Sign raw data without personal_sign prefix',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _rawSign : null,
            child: const Text('Raw Sign'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Solana Signing',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          const Text(
            'sol_signMessage',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Sign a message on Solana Devnet',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _signSolanaMessage : null,
            child: const Text('Sign Solana Message'),
          ),
          const SizedBox(height: 16),
          const Text(
            'sol_signAndSendTransaction',
            style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
          ),
          const Text(
            'Send 0.01 SOL on Devnet',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _sendSol : null,
            child: const Text('Send 0.01 SOL'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Supported Methods',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey[100],
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Ethereum:',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                Text('  - personal_sign'),
                Text('  - eth_sign'),
                Text('  - eth_signTransaction'),
                Text('  - eth_signTypedData_v3'),
                Text('  - eth_signTypedData_v4'),
                SizedBox(height: 8),
                Text('Solana:', style: TextStyle(fontWeight: FontWeight.bold)),
                Text('  - sol_signMessage'),
                Text('  - sol_signTransaction'),
                Text('  - sol_signAndSendTransaction'),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTransactionsTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text(
            'Fund Testnet Wallet',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Request 0.01 ETH on Sepolia testnet',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _fundTestnet : null,
            child: const Text('Fund Testnet'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Send Transaction',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Send 1 wei to test recipient on Sepolia',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _sendTransaction : null,
            child: const Text('Send 1 Wei'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Send Asset (Gas Sponsorship)',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Text(
            'Send 0.0001 ETH with gas sponsorship${AppSettings.isAccountAbstraction ? " (AA enabled)" : ""}',
            style: const TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _sendAssetWithGasSponsorship : null,
            style: ElevatedButton.styleFrom(
              backgroundColor: AppSettings.isAccountAbstraction ? Colors.green[100] : null,
            ),
            child: const Text('Send with Gas Sponsorship'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Transaction History',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Get recent transactions on Sepolia',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _getTransactions : null,
            child: const Text('Get Transactions'),
          ),
          const SizedBox(height: 24),
          const Text(
            'NFT Assets',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Get NFTs owned by wallet on Sepolia',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _getNftAssets : null,
            child: const Text('Get NFT Assets'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Transaction Security',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'Evaluate transaction using Blockaid security scan',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _hasWallet ? _evaluateTransaction : null,
            child: const Text('Evaluate Transaction'),
          ),
          const SizedBox(height: 24),
          const Text(
            'Chain Info',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey[100],
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Ethereum Sepolia Testnet',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                Text('  Chain ID: eip155:11155111'),
                SizedBox(height: 8),
                Text('Solana Devnet',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                Text('  Chain ID: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
297
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for Portal - Stablecoin wallet infrastructure

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface, portal_flutter_android, portal_flutter_ios

More

Packages that depend on portal_flutter

Packages that implement portal_flutter