near_dart 0.3.0 copy "near_dart: ^0.3.0" to clipboard
near_dart: ^0.3.0 copied to clipboard

Complete NEAR Protocol SDK for Flutter/Dart. RPC client, wallet integration, and type-safe primitives.

example/lib/main.dart

import 'dart:convert';
import 'package:flutter/material.dart' hide Action;
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:near_dart/near_dart.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:app_links/app_links.dart';

import 'connect_wallet_page.dart';
import 'glass.dart';
import 'package:google_fonts/google_fonts.dart';

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

// Callback URL scheme for wallet authentication
const String kCallbackScheme = 'nearsdk';

/// Formats an RPC error for display, including the node's `data` detail
/// (e.g. "State of contract X is too large to be viewed") which is far
/// more useful than the generic "Server error" message alone.
String describeRpcError(RpcError e) {
  final data = e.data;
  if (data != null && '$data'.isNotEmpty && '$data' != 'null') {
    return '${e.message}: $data';
  }
  return e.message;
}

// NEAR Official Colors
/// Legacy color tokens, repointed to the dark "NEAR Terminal" glass palette
/// (see [Near] in glass.dart). Kept so existing screens restyle automatically.
class NearTheme {
  static const black = Near.textPrimary; // primary text (light on dark)
  static const white = Near.textPrimary; // foreground text/icons
  static const green = Near.mint;
  static const grey = Near.textMuted;
  static final greyLight = Colors.white.withValues(
    alpha: 0.08,
  ); // fills/borders
  static const greyDark = Near.textMuted;
}

// App State
enum Network { mainnet, testnet }

class AppState extends ChangeNotifier {
  Network _network = Network.testnet;
  NearRpcClient? _client;

  // Wallet connection state
  String? _connectedAccountId;
  String? _publicKey;
  String? _lastCallbackUrl;
  String? _lastCallbackType;

  Network get network => _network;
  String get networkName => _network == Network.mainnet ? 'Mainnet' : 'Testnet';
  String? get connectedAccountId => _connectedAccountId;
  String? get publicKey => _publicKey;
  String? get lastCallbackUrl => _lastCallbackUrl;
  String? get lastCallbackType => _lastCallbackType;
  bool get isConnected => _connectedAccountId != null;

  NearRpcClient get client {
    _client ??= _network == Network.mainnet
        ? NearRpcClient.mainnet()
        : NearRpcClient.testnet();
    return _client!;
  }

  void switchNetwork(Network network) {
    if (_network != network) {
      _network = network;
      _client?.close();
      _client = null;
      // Clear connection when switching networks
      _connectedAccountId = null;
      _publicKey = null;
      notifyListeners();
    }
  }

  void handleWalletCallback(Uri uri) {
    _lastCallbackUrl = uri.toString();

    // Parse the callback
    final path = uri.path;
    final params = uri.queryParameters;

    if (path.contains('success')) {
      _lastCallbackType = 'success';
      // MyNearWallet returns account_id and public_key in success callback
      if (params.containsKey('account_id')) {
        _connectedAccountId = params['account_id'];
      }
      if (params.containsKey('public_key')) {
        _publicKey = params['public_key'];
      }
      // Sometimes the account_id is in all_keys format
      if (params.containsKey('all_keys')) {
        _publicKey = params['all_keys'];
      }
    } else if (path.contains('failure')) {
      _lastCallbackType = 'failure';
    } else if (path.contains('tx')) {
      _lastCallbackType = 'transaction';
      // Transaction callback may contain transactionHashes
    }

    notifyListeners();
  }

  void disconnect() {
    _connectedAccountId = null;
    _publicKey = null;
    _lastCallbackUrl = null;
    _lastCallbackType = null;
    notifyListeners();
  }

  @override
  void dispose() {
    _client?.close();
    super.dispose();
  }
}

// Main App
class NearSdkDemo extends StatefulWidget {
  const NearSdkDemo({super.key});

  @override
  State<NearSdkDemo> createState() => _NearSdkDemoState();
}

class _NearSdkDemoState extends State<NearSdkDemo> {
  final _appState = AppState();
  final _navKey = GlobalKey<NavigatorState>();
  AppLinks? _appLinks;

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

  /// Processes the wallet sign-in callback wherever the redirect lands:
  /// on web the initial app URL carries it; on mobile it arrives as a
  /// `nearsdk://` deep link (initial + while running).
  Future<void> _initWalletCallbacks() async {
    if (kIsWeb) {
      if (looksLikeWalletCallback(Uri.base)) {
        await _processCallback(Uri.base);
      }
      return;
    }
    _appLinks = AppLinks();
    final initial = await _appLinks!.getInitialLink();
    if (initial != null) await _processCallback(initial);
    _appLinks!.uriLinkStream.listen(_processCallback);
  }

  Future<void> _processCallback(Uri uri) async {
    final account = await handleWalletCallback(uri);
    final context = _navKey.currentContext;
    if (account != null && context != null && context.mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Wallet connected: ${account.accountId.value}'),
          backgroundColor: NearTheme.green,
        ),
      );
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _appState,
      builder: (context, _) {
        return MaterialApp(
          navigatorKey: _navKey,
          title: 'NEAR SDK Demo',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            brightness: Brightness.dark,
            scaffoldBackgroundColor: Colors.transparent,
            primaryColor: Near.mint,
            colorScheme: const ColorScheme.dark(
              primary: Near.mint,
              onPrimary: Near.canvas,
              surface: Near.ink,
              brightness: Brightness.dark,
            ),
            textTheme:
                GoogleFonts.hankenGroteskTextTheme(
                  ThemeData.dark().textTheme,
                ).apply(
                  bodyColor: Near.textPrimary,
                  displayColor: Near.textPrimary,
                ),
            appBarTheme: AppBarTheme(
              backgroundColor: Colors.transparent,
              foregroundColor: Near.textPrimary,
              elevation: 0,
              centerTitle: false,
              titleTextStyle: Near.display(20),
            ),
            iconTheme: const IconThemeData(color: Near.textPrimary),
            inputDecorationTheme: InputDecorationTheme(
              filled: true,
              fillColor: Colors.white.withValues(alpha: 0.05),
              hintStyle: Near.body(13, color: Near.textMuted),
              labelStyle: Near.body(13, color: Near.textMuted),
              floatingLabelStyle: Near.body(12, color: Near.mint),
              enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(14),
                borderSide: BorderSide(color: Near.glassBorder),
              ),
              focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(14),
                borderSide: const BorderSide(color: Near.mint, width: 1.4),
              ),
              contentPadding: const EdgeInsets.symmetric(
                horizontal: 16,
                vertical: 14,
              ),
            ),
          ),
          // Living glass canvas behind everything, content centered to a
          // phone-width column so it looks right on wide desktop/web screens.
          builder: (context, child) {
            return AnimatedGlassBackground(
              child: Center(
                child: ConstrainedBox(
                  constraints: const BoxConstraints(maxWidth: 580),
                  child: child ?? const SizedBox.shrink(),
                ),
              ),
            );
          },
          home: HomePage(appState: _appState),
        );
      },
    );
  }
}

// Home feature descriptor
class _Feature {
  const _Feature(
    this.icon,
    this.title,
    this.subtitle,
    this.build, {
    this.featured = false,
  });
  final IconData icon;
  final String title;
  final String subtitle;
  final Widget Function() build;
  final bool featured;
}

// Home Page
class HomePage extends StatelessWidget {
  final AppState appState;

  const HomePage({super.key, required this.appState});

  @override
  Widget build(BuildContext context) {
    final testnet = appState.network == Network.testnet;
    final features = <_Feature>[
      _Feature(
        Icons.bolt,
        'Sign & Send',
        'Local key → Borsh → send_tx, on-chain',
        () => LocalSigningPage(appState: appState),
        featured: true,
      ),
      _Feature(
        Icons.account_balance_wallet_outlined,
        'Connect Wallet',
        'MyNearWallet redirect, then sign locally',
        () => ConnectWalletPage(
          isTestnet: testnet,
          contractId: testnet
              ? 'guestbook.near-examples.testnet'
              : 'social.near',
        ),
        featured: true,
      ),
      _Feature(
        Icons.login,
        'Sign in with NEAR',
        'NEP-413 session auth vs a live API',
        () => SignInWithNearPage(appState: appState),
        featured: true,
      ),
      _Feature(
        Icons.wifi,
        'Network Status',
        'status() — node, sync, protocol',
        () => NetworkStatusPage(appState: appState),
      ),
      _Feature(
        Icons.account_circle_outlined,
        'Account Explorer',
        'viewAccount() — balance, storage, code',
        () => AccountExplorerPage(appState: appState),
      ),
      _Feature(
        Icons.key_outlined,
        'Access Keys',
        'viewAccessKeyList() — full & function-call',
        () => AccessKeysPage(appState: appState),
      ),
      _Feature(
        Icons.code,
        'Contract Calls',
        'callFunction() — ft_metadata, balances',
        () => ContractCallsPage(appState: appState),
      ),
      _Feature(
        Icons.how_to_vote_outlined,
        'Validators',
        'validators() — epoch, stake',
        () => ValidatorsPage(appState: appState),
      ),
      _Feature(
        Icons.view_in_ar_outlined,
        'Block Explorer',
        'block() — header, chunks',
        () => BlockExplorerPage(appState: appState),
      ),
      _Feature(
        Icons.local_gas_station_outlined,
        'Gas Price',
        'gasPrice() — yoctoNEAR/gas',
        () => GasPricePage(appState: appState),
      ),
      _Feature(
        Icons.data_object,
        'Code & State',
        'viewCode(), viewState()',
        () => CodeStatePage(appState: appState),
      ),
      _Feature(
        Icons.send_outlined,
        'Transaction Builder',
        'Build & serialize actions',
        () => TransactionBuilderPage(appState: appState),
      ),
      _Feature(
        Icons.link,
        'Wallet URLs',
        'MyNearWallet URL building',
        () => WalletUrlPage(appState: appState),
      ),
      _Feature(
        Icons.web,
        'Legacy WebView',
        'Embedded flow — mobile only',
        () => WalletConnectPage(appState: appState),
      ),
    ];

    return Scaffold(
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
          children: [
            // Hero
            Reveal(
              index: 0,
              child: Row(
                children: [
                  const NearMark(size: 44),
                  const SizedBox(width: 14),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('NEAR', style: Near.display(26)),
                        Text(
                          'Flutter SDK',
                          style: Near.mono(12, color: Near.textMuted),
                        ),
                      ],
                    ),
                  ),
                  _NetworkSwitch(appState: appState),
                ],
              ),
            ),
            const SizedBox(height: 22),
            Reveal(
              index: 1,
              child: Text(
                'One Dart codebase. Sign, send and connect on mobile, web '
                'and desktop.',
                style: Near.body(14.5, color: Near.textMuted),
              ),
            ),
            const SizedBox(height: 22),
            for (var i = 0; i < features.length; i++)
              Padding(
                padding: const EdgeInsets.only(bottom: 12),
                child: Reveal(
                  index: i + 2,
                  child: _FeatureCard(
                    icon: features[i].icon,
                    title: features[i].title,
                    subtitle: features[i].subtitle,
                    featured: features[i].featured,
                    onTap: () => _push(context, features[i].build()),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  void _push(BuildContext context, Widget page) {
    Navigator.push(
      context,
      PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 380),
        reverseTransitionDuration: const Duration(milliseconds: 280),
        pageBuilder: (_, a, __) => page,
        transitionsBuilder: (_, a, __, child) {
          final curved = CurvedAnimation(parent: a, curve: Curves.easeOutCubic);
          return FadeTransition(
            opacity: curved,
            child: SlideTransition(
              position: Tween(
                begin: const Offset(0, 0.04),
                end: Offset.zero,
              ).animate(curved),
              child: child,
            ),
          );
        },
      ),
    );
  }
}

// Network Switch
class _NetworkSwitch extends StatelessWidget {
  final AppState appState;

  const _NetworkSwitch({required this.appState});

  @override
  Widget build(BuildContext context) {
    final mainnet = appState.network == Network.mainnet;
    return PressFx(
      onTap: () =>
          appState.switchNetwork(mainnet ? Network.testnet : Network.mainnet),
      child: GlassChip(
        label: appState.networkName.toUpperCase(),
        color: mainnet ? Near.mint : const Color(0xFFFFC861),
      ),
    );
  }
}

// Feature Card — frosted glass row with mint accent for featured items.
class _FeatureCard extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final VoidCallback onTap;
  final bool featured;

  const _FeatureCard({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.onTap,
    this.featured = false,
  });

  @override
  Widget build(BuildContext context) {
    return PressFx(
      onTap: onTap,
      child: GlassPanel(
        padding: const EdgeInsets.all(16),
        strong: featured,
        glow: featured ? Near.mint : null,
        borderColor: featured ? Near.mint.withValues(alpha: 0.45) : null,
        child: Row(
          children: [
            Container(
              width: 46,
              height: 46,
              decoration: BoxDecoration(
                color: featured
                    ? Near.mint.withValues(alpha: 0.16)
                    : Colors.white.withValues(alpha: 0.06),
                borderRadius: BorderRadius.circular(13),
                border: Border.all(
                  color: featured
                      ? Near.mint.withValues(alpha: 0.4)
                      : Near.glassBorder,
                ),
              ),
              child: Icon(
                icon,
                color: featured ? Near.mint : Near.textPrimary,
                size: 22,
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: Near.body(
                      15.5,
                      w: FontWeight.w700,
                      color: featured ? Near.mint : Near.textPrimary,
                    ),
                  ),
                  const SizedBox(height: 3),
                  Text(subtitle, style: Near.body(12.5, color: Near.textMuted)),
                ],
              ),
            ),
            Icon(
              Icons.arrow_outward_rounded,
              size: 18,
              color: featured ? Near.mint : Near.textMuted,
            ),
          ],
        ),
      ),
    );
  }
}

// Base Page — frosted app bar over the living glass canvas.
class _BasePage extends StatelessWidget {
  final String title;
  final AppState appState;
  final Widget child;

  const _BasePage({
    required this.title,
    required this.appState,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: appState,
      builder: (context, _) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18),
              onPressed: () => Navigator.maybePop(context),
            ),
            titleSpacing: 0,
            title: Text(title, style: Near.display(18)),
            actions: [
              _NetworkSwitch(appState: appState),
              const SizedBox(width: 16),
            ],
          ),
          body: SafeArea(child: child),
        );
      },
    );
  }
}

// Result card — glass panel with copy action + mono key/value rows.
class _ResultCard extends StatelessWidget {
  final String title;
  final Map<String, dynamic> data;

  const _ResultCard({required this.title, required this.data});

  @override
  Widget build(BuildContext context) {
    return GlassPanel(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Row(
                children: [
                  Container(
                    width: 6,
                    height: 6,
                    decoration: const BoxDecoration(
                      shape: BoxShape.circle,
                      color: Near.mint,
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    title,
                    style: Near.body(
                      13.5,
                      w: FontWeight.w700,
                      color: Near.textPrimary,
                    ),
                  ),
                ],
              ),
              PressFx(
                onTap: () {
                  Clipboard.setData(
                    ClipboardData(
                      text: const JsonEncoder.withIndent('  ').convert(data),
                    ),
                  );
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      backgroundColor: Near.ink,
                      content: Text(
                        'Copied',
                        style: Near.body(13, color: Near.mint),
                      ),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                },
                child: const Icon(
                  Icons.copy_rounded,
                  size: 16,
                  color: Near.textMuted,
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          ...data.entries.map(
            (e) => Padding(
              padding: const EdgeInsets.only(bottom: 10),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  SizedBox(
                    width: 140,
                    child: Text(
                      e.key,
                      style: Near.mono(11.5, color: Near.textMuted),
                    ),
                  ),
                  Expanded(
                    child: Text(
                      '${e.value}',
                      style: Near.mono(12, color: Near.textPrimary),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// Loading button — alias to the glass primary button.
class _LoadButton extends StatelessWidget {
  final String label;
  final bool isLoading;
  final VoidCallback onPressed;

  const _LoadButton({
    required this.label,
    required this.isLoading,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) =>
      GlassButton(label: label, loading: isLoading, onPressed: onPressed);
}

// Error card — danger-tinted glass.
class _ErrorCard extends StatelessWidget {
  final String message;

  const _ErrorCard({required this.message});

  @override
  Widget build(BuildContext context) {
    return GlassPanel(
      padding: const EdgeInsets.all(14),
      borderColor: Near.danger.withValues(alpha: 0.4),
      child: Row(
        children: [
          const Icon(Icons.error_outline_rounded, color: Near.danger, size: 20),
          const SizedBox(width: 12),
          Expanded(
            child: Text(message, style: Near.body(13, color: Near.danger)),
          ),
        ],
      ),
    );
  }
}

// ============================================================================
// 0. LOCAL SIGNING PAGE — generate key, fund via faucet, sign & send.
//    This is the flagship demo: the whole flow runs locally (incl. on web),
//    no external wallet redirect needed.
// ============================================================================
class LocalSigningPage extends StatefulWidget {
  final AppState appState;

  const LocalSigningPage({super.key, required this.appState});

  @override
  State<LocalSigningPage> createState() => _LocalSigningPageState();
}

class _LocalSigningPageState extends State<LocalSigningPage> {
  final _accountCtrl = TextEditingController();
  final _secretCtrl = TextEditingController();
  final _receiverCtrl = TextEditingController(text: 'testnet');
  final _amountCtrl = TextEditingController(text: '0.001');

  KeyPairEd25519? _keyPair;
  bool _busy = false;
  String? _status;
  String? _error;
  Map<String, dynamic>? _result;
  String? _txHash;

  @override
  void dispose() {
    _accountCtrl.dispose();
    _secretCtrl.dispose();
    _receiverCtrl.dispose();
    _amountCtrl.dispose();
    super.dispose();
  }

  bool get _isTestnet => widget.appState.network == Network.testnet;

  void _set({bool? busy, String? status, String? error}) {
    setState(() {
      if (busy != null) _busy = busy;
      _status = status;
      _error = error;
    });
  }

  Future<void> _generateKey() async {
    _set(busy: true, status: 'Generating ed25519 key pair…');
    try {
      final kp = await KeyPairEd25519.generate();
      setState(() {
        _keyPair = kp;
        _secretCtrl.text = kp.toString();
      });
      _set(busy: false, status: 'Key generated. Now create a funded account.');
    } catch (e) {
      _set(busy: false, error: '$e');
    }
  }

  Future<void> _createFaucetAccount() async {
    final kp = _keyPair;
    if (kp == null) {
      _set(error: 'Generate a key first.');
      return;
    }
    _set(busy: true, status: 'Requesting a funded testnet account…');
    try {
      final accountId =
          'near-dart-demo-${DateTime.now().millisecondsSinceEpoch}.testnet';
      final response = await http.post(
        Uri.parse('https://helper.testnet.near.org/account'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'newAccountId': accountId,
          'newAccountPublicKey': kp.publicKey.value,
        }),
      );
      if (response.statusCode != 200) {
        _set(busy: false, error: 'Faucet failed (HTTP ${response.statusCode})');
        return;
      }
      _accountCtrl.text = accountId;

      // The faucet account is only optimistically executed; wait until its
      // access key is final-queryable so the next Sign & Send won't race.
      _set(busy: true, status: 'Account created — waiting for it to be ready…');
      var ready = false;
      for (var i = 0; i < 15; i++) {
        final check = await widget.appState.client.viewAccessKey(
          accountId: AccountId(accountId),
          publicKey: kp.publicKey,
          blockReference: BlockReference.finality(Finality.final_),
        );
        if (check.isSuccess) {
          ready = true;
          break;
        }
        await Future<void>.delayed(const Duration(milliseconds: 1200));
      }
      _set(
        busy: false,
        status: ready
            ? 'Funded $accountId is ready. Sign & send now.'
            : 'Account created but not visible yet — try Sign & Send shortly.',
      );
    } catch (e) {
      _set(busy: false, error: '$e');
    }
  }

  Future<void> _signAndSend() async {
    final accountId = _accountCtrl.text.trim();
    final secret = _secretCtrl.text.trim();
    final receiver = _receiverCtrl.text.trim();
    final amount = _amountCtrl.text.trim();
    if (accountId.isEmpty || secret.isEmpty || receiver.isEmpty) {
      _set(error: 'Account, secret key and receiver are required.');
      return;
    }
    setState(() {
      _result = null;
      _txHash = null;
    });
    _set(busy: true, status: 'Signing locally and broadcasting via send_tx…');
    try {
      final keyPair = await KeyPairEd25519.fromString(secret);
      final account = Account(
        accountId: AccountId(accountId),
        keyPair: keyPair,
        client: widget.appState.client,
      );
      final result = await account.transfer(
        receiverId: AccountId(receiver),
        amount: NearToken.parse(amount),
      );

      switch (result) {
        case RpcSuccess(:final value):
          setState(() {
            _txHash = value.transaction.hash;
            _result = {
              'status': _statusLabel(value.status),
              'tx_hash': value.transaction.hash,
              'signer': value.transaction.signerId,
              'gas_burnt': value.transactionOutcome.outcome.gasBurnt,
            };
          });
          _set(busy: false, status: 'Executed on-chain ✓');
        case RpcFailure(:final error):
          _set(
            busy: false,
            error: 'send_tx failed: ${describeRpcError(error)}',
          );
      }
    } catch (e) {
      _set(busy: false, error: '$e');
    }
  }

  // Don't use runtimeType.toString() — it returns minified garbage like
  // "minified:anL" in release/web builds. Match the sealed type instead.
  String _statusLabel(TransactionStatus s) => switch (s) {
    TransactionStatusSuccess() => 'Success',
    TransactionStatusSuccessReceipt() => 'Success (receipt pending)',
    TransactionStatusFailure() => 'Failure',
    TransactionStatusUnknown() => 'Unknown',
  };

  Future<void> _openExplorer() async {
    final hash = _txHash;
    if (hash == null) return;
    final base = _isTestnet
        ? 'https://testnet.nearblocks.io'
        : 'https://nearblocks.io';
    await launchUrl(
      Uri.parse('$base/txns/$hash'),
      mode: LaunchMode.externalApplication,
    );
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Sign & Send',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            'The full flow runs locally — key generation, Borsh '
            'serialization, ed25519 signing and broadcasting. No wallet '
            'redirect. Works on web, mobile and desktop.',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 20),

          _stepLabel('1', 'Generate a key pair'),
          const SizedBox(height: 8),
          _LoadButton(
            label: 'Generate ed25519 key',
            isLoading: _busy,
            onPressed: _generateKey,
          ),
          if (_keyPair != null) ...[
            const SizedBox(height: 8),
            _ResultCard(
              title: 'Key pair',
              data: {
                'public_key': _keyPair!.publicKey.value,
                'secret_key': '${_keyPair!.toString().substring(0, 20)}…',
              },
            ),
          ],
          const SizedBox(height: 20),

          _stepLabel('2', 'Get a funded account'),
          const SizedBox(height: 8),
          if (_isTestnet)
            _LoadButton(
              label: 'Create funded testnet account (faucet)',
              isLoading: _busy,
              onPressed: _createFaucetAccount,
            )
          else
            Text(
              'Faucet is testnet-only. On mainnet, import an existing '
              'account + secret key below.',
              style: TextStyle(color: NearTheme.grey, fontSize: 12),
            ),
          const SizedBox(height: 12),
          _field(_accountCtrl, 'Account ID', 'alice.testnet'),
          const SizedBox(height: 8),
          _field(_secretCtrl, 'Secret key', 'ed25519:…', mono: true),
          const SizedBox(height: 20),

          _stepLabel('3', 'Sign & send a transfer'),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                flex: 2,
                child: _field(_receiverCtrl, 'Receiver', 'bob.testnet'),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: _field(_amountCtrl, 'NEAR', '0.001', number: true),
              ),
            ],
          ),
          const SizedBox(height: 12),
          _LoadButton(
            label: 'Sign & Send',
            isLoading: _busy,
            onPressed: _signAndSend,
          ),

          if (_status != null) ...[
            const SizedBox(height: 16),
            Text(
              _status!,
              style: TextStyle(
                color: NearTheme.greyDark,
                fontSize: 13,
                fontWeight: FontWeight.w500,
              ),
            ),
          ],
          if (_error != null) ...[
            const SizedBox(height: 12),
            _ErrorCard(message: _error!),
          ],
          if (_result != null) ...[
            const SizedBox(height: 12),
            _ResultCard(title: 'Transaction result', data: _result!),
            const SizedBox(height: 8),
            OutlinedButton.icon(
              onPressed: _openExplorer,
              icon: const Icon(Icons.open_in_new, size: 18),
              label: const Text('View on explorer'),
            ),
          ],
        ],
      ),
    );
  }

  Widget _stepLabel(String n, String text) => Row(
    children: [
      CircleAvatar(
        radius: 12,
        backgroundColor: NearTheme.green,
        child: Text(
          n,
          style: const TextStyle(
            color: NearTheme.black,
            fontSize: 12,
            fontWeight: FontWeight.w700,
          ),
        ),
      ),
      const SizedBox(width: 10),
      Text(
        text,
        style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
      ),
    ],
  );

  Widget _field(
    TextEditingController ctrl,
    String label,
    String hint, {
    bool mono = false,
    bool number = false,
  }) => TextField(
    controller: ctrl,
    keyboardType: number
        ? const TextInputType.numberWithOptions(decimal: true)
        : TextInputType.text,
    style: TextStyle(fontSize: 13, fontFamily: mono ? 'monospace' : null),
    decoration: InputDecoration(
      labelText: label,
      hintText: hint,
      isDense: true,
      border: const OutlineInputBorder(),
    ),
  );
}

// ============================================================================
// 1. NETWORK STATUS PAGE
// ============================================================================
class NetworkStatusPage extends StatefulWidget {
  final AppState appState;
  const NetworkStatusPage({super.key, required this.appState});

  @override
  State<NetworkStatusPage> createState() => _NetworkStatusPageState();
}

class _NetworkStatusPageState extends State<NetworkStatusPage> {
  bool _loading = false;
  String? _error;
  StatusResponse? _status;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      // Clear data when network changes
      if (mounted) {
        setState(() {
          _status = null;
          _error = null;
        });
      }
    }
  }

  Future<void> _fetch() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.status();

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _status = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Network Status',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: status()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          _LoadButton(
            label: 'Fetch Status',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_status != null) ...[
            _ResultCard(
              title: 'Node Info',
              data: {
                'chain_id': _status!.chainId,
                'protocol_version': _status!.protocolVersion,
                'node_version': _status!.version.version,
                'build': _status!.version.build ?? 'N/A',
              },
            ),
            const SizedBox(height: 12),
            _ResultCard(
              title: 'Sync Info',
              data: {
                'latest_block_height': _status!.syncInfo.latestBlockHeight,
                'latest_block_hash': _status!.syncInfo.latestBlockHash,
                'syncing': _status!.syncInfo.syncing,
                'earliest_block_height': _status!.syncInfo.earliestBlockHeight,
              },
            ),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 2. ACCOUNT EXPLORER PAGE
// ============================================================================
class AccountExplorerPage extends StatefulWidget {
  final AppState appState;
  const AccountExplorerPage({super.key, required this.appState});

  @override
  State<AccountExplorerPage> createState() => _AccountExplorerPageState();
}

class _AccountExplorerPageState extends State<AccountExplorerPage> {
  final _controller = TextEditingController();
  bool _loading = false;
  String? _error;
  AccountView? _account;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    _controller.text = widget.appState.network == Network.mainnet
        ? 'near'
        : 'testnet';
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    _controller.dispose();
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _account = null;
          _error = null;
          _controller.text = widget.appState.network == Network.mainnet
              ? 'near'
              : 'testnet';
        });
      }
    }
  }

  Future<void> _fetch() async {
    if (_controller.text.isEmpty) return;

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

    final result = await widget.appState.client.viewAccount(
      accountId: AccountId(_controller.text),
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _account = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Account Explorer',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: viewAccount()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _controller,
            decoration: const InputDecoration(hintText: 'Account ID'),
            onSubmitted: (_) => _fetch(),
          ),
          const SizedBox(height: 12),
          _LoadButton(
            label: 'Fetch Account',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_account != null)
            _ResultCard(
              title: 'Account: ${_controller.text}',
              data: {
                'balance':
                    '${_account!.amount.toNear().toStringAsFixed(4)} NEAR',
                'balance_yocto': _account!.amount.yoctoNear.toString(),
                'locked':
                    '${_account!.locked.toNear().toStringAsFixed(4)} NEAR',
                'storage_usage': '${_account!.storageUsage} bytes',
                'has_contract': _account!.hasContract,
                'code_hash': _account!.codeHash.value,
                'block_height': _account!.blockHeight,
              },
            ),
        ],
      ),
    );
  }
}

// ============================================================================
// 3. ACCESS KEYS PAGE
// ============================================================================
class AccessKeysPage extends StatefulWidget {
  final AppState appState;
  const AccessKeysPage({super.key, required this.appState});

  @override
  State<AccessKeysPage> createState() => _AccessKeysPageState();
}

class _AccessKeysPageState extends State<AccessKeysPage> {
  final _controller = TextEditingController();
  bool _loading = false;
  String? _error;
  List<AccessKeyInfoView>? _keys;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    _controller.text = widget.appState.network == Network.mainnet
        ? 'near'
        : 'testnet';
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    _controller.dispose();
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _keys = null;
          _error = null;
          _controller.text = widget.appState.network == Network.mainnet
              ? 'near'
              : 'testnet';
        });
      }
    }
  }

  Future<void> _fetch() async {
    if (_controller.text.isEmpty) return;

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

    final result = await widget.appState.client.viewAccessKeyList(
      accountId: AccountId(_controller.text),
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _keys = result.getOrThrow().keys;
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Access Keys',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: viewAccessKeyList()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _controller,
            decoration: const InputDecoration(hintText: 'Account ID'),
            onSubmitted: (_) => _fetch(),
          ),
          const SizedBox(height: 12),
          _LoadButton(
            label: 'Fetch Keys',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_keys != null) ...[
            Text(
              'Found ${_keys!.length} key(s)',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 12),
            ..._keys!.map((key) {
              final isFullAccess =
                  key.accessKey.permission is FullAccessPermissionView;
              return Card(
                margin: const EdgeInsets.only(bottom: 8),
                child: Padding(
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(
                            isFullAccess ? Icons.key : Icons.vpn_key_outlined,
                            size: 16,
                            color: isFullAccess
                                ? Colors.orange
                                : NearTheme.green,
                          ),
                          const SizedBox(width: 8),
                          Text(
                            isFullAccess ? 'Full Access' : 'Function Call',
                            style: TextStyle(
                              fontWeight: FontWeight.w600,
                              color: isFullAccess
                                  ? Colors.orange
                                  : NearTheme.green,
                              fontSize: 13,
                            ),
                          ),
                          const Spacer(),
                          Text(
                            'Nonce: ${key.accessKey.nonce}',
                            style: TextStyle(
                              color: NearTheme.grey,
                              fontSize: 12,
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      Text(
                        key.publicKey,
                        style: const TextStyle(
                          fontFamily: 'monospace',
                          fontSize: 11,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                ),
              );
            }),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 4. CONTRACT CALLS PAGE
// ============================================================================
class ContractCallsPage extends StatefulWidget {
  final AppState appState;
  const ContractCallsPage({super.key, required this.appState});

  @override
  State<ContractCallsPage> createState() => _ContractCallsPageState();
}

class _ContractCallsPageState extends State<ContractCallsPage> {
  bool _loading = false;
  String? _error;
  Map<String, dynamic>? _metadata;
  String? _totalSupply;
  String? _balance;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _metadata = null;
          _totalSupply = null;
          _balance = null;
          _error = null;
        });
      }
    }
  }

  String get _wrapContract =>
      widget.appState.network == Network.mainnet ? 'wrap.near' : 'wrap.testnet';

  Future<void> _fetchMetadata() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.callFunction(
      accountId: AccountId(_wrapContract),
      methodName: 'ft_metadata',
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _metadata = result.getOrThrow().resultAsJson() as Map<String, dynamic>;
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  Future<void> _fetchTotalSupply() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.callFunction(
      accountId: AccountId(_wrapContract),
      methodName: 'ft_total_supply',
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _totalSupply = result.getOrThrow().resultAsJson() as String;
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  Future<void> _fetchBalance() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final checkAccount = widget.appState.network == Network.mainnet
        ? 'near'
        : 'testnet';

    final result = await widget.appState.client.callFunction(
      accountId: AccountId(_wrapContract),
      methodName: 'ft_balance_of',
      args: {'account_id': checkAccount},
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _balance = result.getOrThrow().resultAsJson() as String;
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Contract Calls',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: callFunction() on $_wrapContract',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 20),

          // ft_metadata
          _LoadButton(
            label: 'Call ft_metadata()',
            isLoading: _loading,
            onPressed: _fetchMetadata,
          ),
          if (_metadata != null) ...[
            const SizedBox(height: 12),
            _ResultCard(title: 'ft_metadata', data: _metadata!),
          ],

          const SizedBox(height: 16),

          // ft_total_supply
          _LoadButton(
            label: 'Call ft_total_supply()',
            isLoading: _loading,
            onPressed: _fetchTotalSupply,
          ),
          if (_totalSupply != null) ...[
            const SizedBox(height: 12),
            _ResultCard(
              title: 'ft_total_supply',
              data: {'supply': _totalSupply},
            ),
          ],

          const SizedBox(height: 16),

          // ft_balance_of
          _LoadButton(
            label: 'Call ft_balance_of()',
            isLoading: _loading,
            onPressed: _fetchBalance,
          ),
          if (_balance != null) ...[
            const SizedBox(height: 12),
            _ResultCard(title: 'ft_balance_of', data: {'balance': _balance}),
          ],

          if (_error != null) ...[
            const SizedBox(height: 12),
            _ErrorCard(message: _error!),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 5. VALIDATORS PAGE
// ============================================================================
class ValidatorsPage extends StatefulWidget {
  final AppState appState;
  const ValidatorsPage({super.key, required this.appState});

  @override
  State<ValidatorsPage> createState() => _ValidatorsPageState();
}

class _ValidatorsPageState extends State<ValidatorsPage> {
  bool _loading = false;
  String? _error;
  ValidatorsResponse? _validators;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _validators = null;
          _error = null;
        });
      }
    }
  }

  Future<void> _fetch() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.validators();

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _validators = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Validators',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: validators()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          _LoadButton(
            label: 'Fetch Validators',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_validators != null) ...[
            _ResultCard(
              title: 'Epoch Info',
              data: {
                'epoch_height': _validators!.epochHeight,
                'epoch_start_height': _validators!.epochStartHeight,
                'current_validators': _validators!.currentValidators.length,
                'next_validators': _validators!.nextValidators.length,
              },
            ),
            const SizedBox(height: 16),
            Text(
              'Top 10 Validators',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 12),
            ..._validators!.currentValidators.take(10).map((v) {
              final stakeNear = v.stake.toNear();
              return Card(
                margin: const EdgeInsets.only(bottom: 8),
                child: Padding(
                  padding: const EdgeInsets.all(12),
                  child: Row(
                    children: [
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              v.accountId,
                              style: const TextStyle(
                                fontWeight: FontWeight.w500,
                                fontSize: 13,
                              ),
                            ),
                            Text(
                              '${(stakeNear / 1000000).toStringAsFixed(2)}M NEAR staked',
                              style: TextStyle(
                                color: NearTheme.grey,
                                fontSize: 12,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 6. BLOCK EXPLORER PAGE
// ============================================================================
class BlockExplorerPage extends StatefulWidget {
  final AppState appState;
  const BlockExplorerPage({super.key, required this.appState});

  @override
  State<BlockExplorerPage> createState() => _BlockExplorerPageState();
}

class _BlockExplorerPageState extends State<BlockExplorerPage> {
  bool _loading = false;
  String? _error;
  BlockResponse? _block;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _block = null;
          _error = null;
        });
      }
    }
  }

  Future<void> _fetch() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.block(
      BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _block = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Block Explorer',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: block()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          _LoadButton(
            label: 'Fetch Latest Block',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_block != null) ...[
            _ResultCard(
              title: 'Block Header',
              data: {
                'height': _block!.header.height,
                'hash': _block!.header.hash,
                'prev_hash': _block!.header.prevHash,
                'timestamp': DateTime.fromMillisecondsSinceEpoch(
                  _block!.header.timestamp ~/ 1000000,
                ).toIso8601String(),
                'author': _block!.author,
              },
            ),
            const SizedBox(height: 16),
            Text(
              'Chunks (${_block!.chunks.length})',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 12),
            ..._block!.chunks.map(
              (c) => Card(
                margin: const EdgeInsets.only(bottom: 8),
                child: Padding(
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Shard ${c.shardId}',
                        style: const TextStyle(fontWeight: FontWeight.w500),
                      ),
                      Text(
                        'Hash: ${c.chunkHash.substring(0, 20)}...',
                        style: TextStyle(
                          color: NearTheme.grey,
                          fontSize: 12,
                          fontFamily: 'monospace',
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 7. GAS PRICE PAGE
// ============================================================================
class GasPricePage extends StatefulWidget {
  final AppState appState;
  const GasPricePage({super.key, required this.appState});

  @override
  State<GasPricePage> createState() => _GasPricePageState();
}

class _GasPricePageState extends State<GasPricePage> {
  bool _loading = false;
  String? _error;
  GasPriceResponse? _gasPrice;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _gasPrice = null;
          _error = null;
        });
      }
    }
  }

  Future<void> _fetch() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.gasPrice();

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _gasPrice = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Gas Price',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: gasPrice()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          _LoadButton(
            label: 'Fetch Gas Price',
            isLoading: _loading,
            onPressed: _fetch,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_gasPrice != null)
            _ResultCard(
              title: 'Current Gas Price',
              data: {
                'gas_price': '${_gasPrice!.gasPrice} yoctoNEAR/gas',
                'gas_price_readable':
                    '${(_gasPrice!.gasPrice.toDouble() / 1e12).toStringAsFixed(6)} TGas',
              },
            ),
        ],
      ),
    );
  }
}

// ============================================================================
// 8. CODE & STATE PAGE
// ============================================================================
class CodeStatePage extends StatefulWidget {
  final AppState appState;
  const CodeStatePage({super.key, required this.appState});

  @override
  State<CodeStatePage> createState() => _CodeStatePageState();
}

class _CodeStatePageState extends State<CodeStatePage> {
  final _controller = TextEditingController();
  bool _loading = false;
  String? _error;
  ContractCodeResponse? _code;
  ContractStateResponse? _state;
  Network? _lastNetwork;

  @override
  void initState() {
    super.initState();
    _lastNetwork = widget.appState.network;
    _controller.text = widget.appState.network == Network.mainnet
        ? 'wrap.near'
        : 'wrap.testnet';
    widget.appState.addListener(_onNetworkChange);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onNetworkChange);
    _controller.dispose();
    super.dispose();
  }

  void _onNetworkChange() {
    if (widget.appState.network != _lastNetwork) {
      _lastNetwork = widget.appState.network;
      if (mounted) {
        setState(() {
          _code = null;
          _state = null;
          _error = null;
          _controller.text = widget.appState.network == Network.mainnet
              ? 'wrap.near'
              : 'wrap.testnet';
        });
      }
    }
  }

  Future<void> _fetchCode() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.viewCode(
      accountId: AccountId(_controller.text),
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _code = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  Future<void> _fetchState() async {
    setState(() {
      _loading = true;
      _error = null;
    });

    final result = await widget.appState.client.viewState(
      accountId: AccountId(_controller.text),
      prefixBase64: '',
      blockReference: BlockReference.finality(Finality.final_),
    );

    setState(() {
      _loading = false;
      if (result.isSuccess) {
        _state = result.getOrThrow();
      } else {
        _error = describeRpcError((result as RpcFailure).error);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Code & State',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'RPC: viewCode(), viewState()',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _controller,
            decoration: const InputDecoration(hintText: 'Contract Account ID'),
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: _LoadButton(
                  label: 'Fetch Code',
                  isLoading: _loading,
                  onPressed: _fetchCode,
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: _LoadButton(
                  label: 'Fetch State',
                  isLoading: _loading,
                  onPressed: _fetchState,
                ),
              ),
            ],
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_code != null)
            _ResultCard(
              title: 'Contract Code',
              data: {
                'hash': _code!.hash,
                'code_size': '${_code!.codeBase64.length} bytes (base64)',
              },
            ),
          if (_state != null) ...[
            const SizedBox(height: 12),
            _ResultCard(
              title: 'Contract State',
              data: {
                'entries': _state!.values.length,
                'block_height': _state!.blockHeight,
              },
            ),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// 9. TRANSACTION BUILDER PAGE
// ============================================================================
class TransactionBuilderPage extends StatefulWidget {
  final AppState appState;
  const TransactionBuilderPage({super.key, required this.appState});

  @override
  State<TransactionBuilderPage> createState() => _TransactionBuilderPageState();
}

class _TransactionBuilderPageState extends State<TransactionBuilderPage> {
  final _signer = TextEditingController(text: 'alice.testnet');
  final _receiver = TextEditingController(text: 'bob.testnet');
  final _amount = TextEditingController(text: '1');
  String? _json;

  void _build() {
    final tx = Transaction(
      signerId: AccountId(_signer.text),
      receiverId: AccountId(_receiver.text),
      actions: [
        TransferAction(
          deposit: NearToken.fromNear(int.tryParse(_amount.text) ?? 1),
        ),
      ],
    );

    setState(() {
      _json = const JsonEncoder.withIndent('  ').convert(tx.toJson());
    });
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Transaction Builder',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'Build & serialize transactions',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _signer,
            decoration: const InputDecoration(labelText: 'Signer ID'),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _receiver,
            decoration: const InputDecoration(labelText: 'Receiver ID'),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _amount,
            decoration: const InputDecoration(labelText: 'Amount (NEAR)'),
            keyboardType: TextInputType.number,
          ),
          const SizedBox(height: 16),
          _LoadButton(
            label: 'Build Transaction',
            isLoading: false,
            onPressed: _build,
          ),
          if (_json != null) ...[
            const SizedBox(height: 20),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Transaction JSON',
                          style: TextStyle(fontWeight: FontWeight.w600),
                        ),
                        IconButton(
                          icon: const Icon(Icons.copy, size: 18),
                          onPressed: () {
                            Clipboard.setData(ClipboardData(text: _json!));
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Copied')),
                            );
                          },
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Text(
                      _json!,
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// NEAR WALLET DATA
// ============================================================================
class NearWallet {
  final String id;
  final String name;
  final String icon;
  final String mainnetUrl;
  final String? testnetUrl;
  final bool supportsTestnet;

  const NearWallet({
    required this.id,
    required this.name,
    required this.icon,
    required this.mainnetUrl,
    this.testnetUrl,
    this.supportsTestnet = false,
  });

  String getUrl(bool isMainnet) {
    if (isMainnet) return mainnetUrl;
    return testnetUrl ?? mainnetUrl;
  }
}

const _nearWallets = [
  NearWallet(
    id: 'mynearwallet',
    name: 'MyNearWallet',
    icon: '🌐',
    mainnetUrl: 'https://app.mynearwallet.com',
    testnetUrl: 'https://testnet.mynearwallet.com',
    supportsTestnet: true,
  ),
  NearWallet(
    id: 'hot',
    name: 'HOT Wallet',
    icon: '🔥',
    mainnetUrl: 'https://wallet.hot.tg',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'meteor',
    name: 'Meteor Wallet',
    icon: '☄️',
    mainnetUrl: 'https://wallet.meteorwallet.app',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'here',
    name: 'HERE Wallet',
    icon: '📍',
    mainnetUrl: 'https://my.herewallet.app',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'sender',
    name: 'Sender Wallet',
    icon: '📤',
    mainnetUrl: 'https://sender.org',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'bitte',
    name: 'Bitte Wallet',
    icon: '💎',
    mainnetUrl: 'https://wallet.bitte.ai',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'mintbase',
    name: 'Mintbase Wallet',
    icon: '🎨',
    mainnetUrl: 'https://wallet.mintbase.xyz',
    testnetUrl: 'https://testnet.wallet.mintbase.xyz',
    supportsTestnet: true,
  ),
  NearWallet(
    id: 'nightly',
    name: 'Nightly Wallet',
    icon: '🌙',
    mainnetUrl: 'https://wallet.nightly.app',
    supportsTestnet: false,
  ),
  NearWallet(
    id: 'welldone',
    name: 'WELLDONE Wallet',
    icon: '✅',
    mainnetUrl: 'https://welldonestudio.io',
    supportsTestnet: false,
  ),
];

// ============================================================================
// 10. WALLET CONNECT PAGE
// ============================================================================
class WalletConnectPage extends StatefulWidget {
  final AppState appState;
  const WalletConnectPage({super.key, required this.appState});

  @override
  State<WalletConnectPage> createState() => _WalletConnectPageState();
}

class _WalletConnectPageState extends State<WalletConnectPage> {
  final _contractController = TextEditingController(text: 'wrap.testnet');
  final _receiverController = TextEditingController(text: 'testnet');
  final _amountController = TextEditingController(text: '0.1');
  NearWallet _selectedWallet = _nearWallets.first;
  bool _sending = false;
  bool _connecting = false;

  @override
  void initState() {
    super.initState();
    _updateContractHint();
    widget.appState.addListener(_onAppStateChanged);
  }

  @override
  void dispose() {
    widget.appState.removeListener(_onAppStateChanged);
    _contractController.dispose();
    _receiverController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  void _onAppStateChanged() {
    if (mounted) {
      setState(() {
        _updateContractHint();
      });
    }
  }

  void _updateContractHint() {
    _contractController.text = widget.appState.network == Network.mainnet
        ? 'wrap.near'
        : 'wrap.testnet';
    _receiverController.text = widget.appState.network == Network.mainnet
        ? 'near'
        : 'testnet';
  }

  String get _walletBaseUrl =>
      _selectedWallet.getUrl(widget.appState.network == Network.mainnet);

  /// Opens an in-app WebView to handle wallet login with callback interception
  Future<void> _connectWallet() async {
    setState(() => _connecting = true);

    try {
      final contract = _contractController.text.isNotEmpty
          ? _contractController.text
          : (widget.appState.network == Network.mainnet
                ? 'wrap.near'
                : 'wrap.testnet');

      // Build the login URL with our custom callback scheme
      final successUrl = '$kCallbackScheme://callback/success';
      final failureUrl = '$kCallbackScheme://callback/failure';

      final loginUrl = Uri.parse('$_walletBaseUrl/login').replace(
        queryParameters: {
          'contract_id': contract,
          'success_url': successUrl,
          'failure_url': failureUrl,
        },
      );

      debugPrint('[WALLET] Opening WebView: ${loginUrl.toString()}');

      // Navigate to the WebView page and wait for result
      final result = await Navigator.push<Map<String, String>?>(
        context,
        MaterialPageRoute(
          builder: (_) => _WalletWebViewPage(
            url: loginUrl.toString(),
            title: 'Connect ${_selectedWallet.name}',
            callbackScheme: kCallbackScheme,
          ),
        ),
      );

      debugPrint('[WALLET] WebView returned: $result');

      if (result != null && mounted) {
        // Successfully got callback data
        final accountId = result['account_id'];
        final publicKey = result['public_key'] ?? result['all_keys'];

        if (accountId != null) {
          widget.appState.handleWalletCallback(
            Uri.parse(
              '$kCallbackScheme://callback/success?account_id=$accountId${publicKey != null ? '&all_keys=$publicKey' : ''}',
            ),
          );

          if (mounted) {
            _showSnack('Connected: $accountId', NearTheme.green);
          }
        } else {
          if (mounted) {
            _showSnack('No account returned from wallet', Colors.orange);
          }
        }
      } else if (mounted) {
        debugPrint('[WALLET] User closed WebView without completing login');
      }
    } catch (e) {
      debugPrint('[WALLET] Error: $e');
      if (mounted) {
        _showSnack('Connection error: $e', Colors.red);
      }
    } finally {
      if (mounted) {
        setState(() => _connecting = false);
      }
    }
  }

  void _showManualConnectDialog() {
    final accountController = TextEditingController();
    bool verifying = false;

    showDialog(
      context: context,
      builder: (ctx) => StatefulBuilder(
        builder: (context, setDialogState) => AlertDialog(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          title: Row(
            children: [
              Icon(Icons.edit, color: NearTheme.green, size: 24),
              const SizedBox(width: 12),
              const Text('Manual Connect'),
            ],
          ),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Enter your NEAR account ID:'),
              const SizedBox(height: 16),
              TextField(
                controller: accountController,
                autofocus: true,
                decoration: InputDecoration(
                  hintText: widget.appState.network == Network.mainnet
                      ? 'your-account.near'
                      : 'your-account.testnet',
                ),
                onSubmitted: (_) async {
                  if (accountController.text.isNotEmpty) {
                    setDialogState(() => verifying = true);
                    await _verifyAndConnect(
                      ctx,
                      accountController.text,
                      setDialogState,
                    );
                  }
                },
              ),
              const SizedBox(height: 8),
              Text(
                'We will verify this account exists',
                style: TextStyle(color: NearTheme.grey, fontSize: 12),
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(ctx),
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: verifying
                  ? null
                  : () async {
                      if (accountController.text.isNotEmpty) {
                        setDialogState(() => verifying = true);
                        await _verifyAndConnect(
                          ctx,
                          accountController.text,
                          setDialogState,
                        );
                      }
                    },
              style: ElevatedButton.styleFrom(
                backgroundColor: NearTheme.green,
                foregroundColor: NearTheme.black,
              ),
              child: verifying
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: NearTheme.black,
                      ),
                    )
                  : const Text('Connect'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _verifyAndConnect(
    BuildContext dialogContext,
    String accountId,
    void Function(void Function()) setDialogState,
  ) async {
    try {
      final result = await widget.appState.client.viewAccount(
        accountId: AccountId(accountId),
        blockReference: BlockReference.finality(Finality.final_),
      );

      if (result.isSuccess) {
        widget.appState.handleWalletCallback(
          Uri.parse(
            '$kCallbackScheme://callback/success?account_id=$accountId',
          ),
        );

        if (dialogContext.mounted) {
          Navigator.pop(dialogContext);
        }
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Connected: $accountId'),
              backgroundColor: NearTheme.green,
            ),
          );
        }
      } else {
        setDialogState(() {});
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(
                'Account not found on ${widget.appState.networkName}',
              ),
              backgroundColor: Colors.red,
            ),
          );
        }
      }
    } catch (e) {
      setDialogState(() {});
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
        );
      }
    }
  }

  Future<void> _sendTransaction() async {
    if (!widget.appState.isConnected) {
      _showSnack('Please connect a wallet first', Colors.orange);
      return;
    }

    final receiver = _receiverController.text.trim();
    if (receiver.isEmpty) {
      _showSnack('Please enter a receiver account ID', Colors.orange);
      return;
    }

    final amountText = _amountController.text.trim();
    final amount = double.tryParse(amountText);
    if (amount == null || amount <= 0) {
      _showSnack('Please enter a valid amount', Colors.orange);
      return;
    }

    setState(() => _sending = true);

    try {
      final accountId = widget.appState.connectedAccountId!;
      var publicKeyStr = widget.appState.publicKey;
      final client = widget.appState.client;

      // --- Step 1: Resolve public key if we don't have one ---
      if (publicKeyStr == null ||
          publicKeyStr.isEmpty ||
          !publicKeyStr.contains(':')) {
        final keysResult = await client.viewAccessKeyList(
          accountId: AccountId(accountId),
          blockReference: BlockReference.finality(Finality.final_),
        );
        if (keysResult.isFailure) {
          _showSnack('Failed to fetch access keys for $accountId', Colors.red);
          return;
        }
        final keys = keysResult.getOrThrow().keys;
        if (keys.isEmpty) {
          _showSnack('No access keys found for $accountId', Colors.red);
          return;
        }
        // Pick first available key
        publicKeyStr = keys.first.publicKey;
      }

      // Handle comma-separated all_keys (take first)
      if (publicKeyStr.contains(',')) {
        publicKeyStr = publicKeyStr.split(',').first.trim();
      }

      // --- Step 2: Get nonce for this key ---
      final keyResult = await client.viewAccessKey(
        accountId: AccountId(accountId),
        publicKey: PublicKey(publicKeyStr),
        blockReference: BlockReference.finality(Finality.final_),
      );
      if (keyResult.isFailure) {
        _showSnack('Failed to get access key nonce', Colors.red);
        return;
      }
      final nonce = keyResult.getOrThrow().nonce + 1;

      // --- Step 3: Get latest block hash ---
      final statusResult = await client.status();
      if (statusResult.isFailure) {
        _showSnack('Failed to get network status', Colors.red);
        return;
      }
      final blockHashBase58 = statusResult
          .getOrThrow()
          .syncInfo
          .latestBlockHash;

      // --- Step 4: Build yoctoNEAR amount ---
      // amount * 10^24 = yoctoNEAR
      // Use BigInt arithmetic to avoid floating point issues
      final parts = amountText.split('.');
      final wholePart = BigInt.parse(parts[0].isEmpty ? '0' : parts[0]);
      BigInt fracPart = BigInt.zero;
      int fracDigits = 0;
      if (parts.length > 1) {
        final frac = parts[1].length > 24
            ? parts[1].substring(0, 24)
            : parts[1];
        fracPart = BigInt.parse(frac.isEmpty ? '0' : frac);
        fracDigits = frac.length;
      }
      final yoctoPerNear = BigInt.from(10).pow(24);
      final amountYocto =
          wholePart * yoctoPerNear +
          fracPart * BigInt.from(10).pow(24 - fracDigits);

      // --- Step 5: Borsh-serialize the transaction (using the SDK) ---
      final txBytes = serializeTransaction(
        Transaction(
          signerId: AccountId(accountId),
          receiverId: AccountId(receiver),
          publicKey: PublicKey(publicKeyStr),
          nonce: BigInt.from(nonce),
          blockHash: CryptoHash(blockHashBase58),
          actions: [
            TransferAction(
              deposit: NearToken.fromYocto(amountYocto.toString()),
            ),
          ],
        ),
      );

      // --- Step 6: Build /sign URL (base64 tx, comma-separated) ---
      final signUrl = Uri.parse('$_walletBaseUrl/sign')
          .replace(
            queryParameters: {
              'transactions': base64Encode(txBytes),
              'callbackUrl': '$kCallbackScheme://callback/tx',
            },
          )
          .toString();

      // --- Step 7: Open WebView ---
      if (!mounted) return;
      final result = await Navigator.push<Map<String, String>?>(
        context,
        MaterialPageRoute(
          builder: (_) => _WalletWebViewPage(
            url: signUrl,
            title: 'Confirm Transaction',
            callbackScheme: kCallbackScheme,
          ),
        ),
      );

      if (mounted) {
        if (result != null && result['transactionHashes'] != null) {
          final txHash = result['transactionHashes']!;
          _showSnack(
            'Sent! Hash: ${txHash.length > 16 ? '${txHash.substring(0, 16)}...' : txHash}',
            NearTheme.green,
          );
        } else if (result != null && result['errorCode'] == null) {
          _showSnack('Transaction submitted', NearTheme.green);
        } else if (result != null && result['errorCode'] != null) {
          _showSnack(
            'Transaction rejected: ${result['errorMessage'] ?? result['errorCode']}',
            Colors.red,
          );
        }
      }
    } catch (e) {
      if (mounted) {
        _showSnack('Error: $e', Colors.red);
      }
    } finally {
      if (mounted) {
        setState(() => _sending = false);
      }
    }
  }

  void _showSnack(String msg, Color color) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: color));
  }

  @override
  Widget build(BuildContext context) {
    final isMainnet = widget.appState.network == Network.mainnet;
    final availableWallets = isMainnet
        ? _nearWallets
        : _nearWallets.where((w) => w.supportsTestnet).toList();

    // Reset to first available wallet if current doesn't support testnet
    if (!availableWallets.contains(_selectedWallet)) {
      _selectedWallet = availableWallets.first;
    }

    return _BasePage(
      title: 'Wallet Connect',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          // Wallet Selector
          Text(
            'SELECT WALLET',
            style: TextStyle(
              color: NearTheme.grey,
              fontSize: 12,
              fontWeight: FontWeight.w600,
              letterSpacing: 1,
            ),
          ),
          const SizedBox(height: 12),

          // Wallet Grid
          GridView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 2.5,
              crossAxisSpacing: 10,
              mainAxisSpacing: 10,
            ),
            itemCount: availableWallets.length,
            itemBuilder: (context, index) {
              final wallet = availableWallets[index];
              final isSelected = wallet.id == _selectedWallet.id;
              return GestureDetector(
                onTap: () => setState(() => _selectedWallet = wallet),
                child: Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 8,
                  ),
                  decoration: BoxDecoration(
                    color: isSelected
                        ? NearTheme.green.withValues(alpha: 0.1)
                        : NearTheme.white,
                    borderRadius: BorderRadius.circular(10),
                    border: Border.all(
                      color: isSelected ? NearTheme.green : NearTheme.greyLight,
                      width: isSelected ? 2 : 1,
                    ),
                  ),
                  child: Row(
                    children: [
                      Text(wallet.icon, style: const TextStyle(fontSize: 20)),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          wallet.name,
                          style: TextStyle(
                            fontWeight: isSelected
                                ? FontWeight.w600
                                : FontWeight.w500,
                            fontSize: 13,
                            color: isSelected
                                ? NearTheme.green
                                : NearTheme.black,
                          ),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      if (isSelected)
                        Icon(
                          Icons.check_circle,
                          color: NearTheme.green,
                          size: 18,
                        ),
                    ],
                  ),
                ),
              );
            },
          ),

          if (!isMainnet) ...[
            const SizedBox(height: 8),
            Text(
              'Only wallets supporting Testnet are shown',
              style: TextStyle(
                color: NearTheme.grey,
                fontSize: 11,
                fontStyle: FontStyle.italic,
              ),
            ),
          ],

          const SizedBox(height: 24),

          // Connection Status (if connected)
          ListenableBuilder(
            listenable: widget.appState,
            builder: (context, _) {
              if (widget.appState.isConnected) {
                return Column(
                  children: [
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: NearTheme.green.withValues(alpha: 0.15),
                        borderRadius: BorderRadius.circular(12),
                        border: Border.all(color: NearTheme.green, width: 2),
                      ),
                      child: Column(
                        children: [
                          Row(
                            children: [
                              Icon(
                                Icons.account_circle,
                                color: NearTheme.green,
                                size: 32,
                              ),
                              const SizedBox(width: 12),
                              Expanded(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    const Text(
                                      'Connected',
                                      style: TextStyle(
                                        color: NearTheme.green,
                                        fontWeight: FontWeight.w600,
                                        fontSize: 12,
                                      ),
                                    ),
                                    Text(
                                      widget.appState.connectedAccountId ??
                                          'Unknown',
                                      style: const TextStyle(
                                        fontWeight: FontWeight.w600,
                                        fontSize: 16,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              IconButton(
                                icon: const Icon(
                                  Icons.logout,
                                  color: Colors.red,
                                ),
                                onPressed: () => widget.appState.disconnect(),
                                tooltip: 'Disconnect',
                              ),
                            ],
                          ),
                          if (widget.appState.publicKey != null) ...[
                            const SizedBox(height: 8),
                            Container(
                              padding: const EdgeInsets.all(8),
                              decoration: BoxDecoration(
                                color: NearTheme.white,
                                borderRadius: BorderRadius.circular(6),
                              ),
                              child: Row(
                                children: [
                                  Icon(
                                    Icons.key,
                                    size: 14,
                                    color: NearTheme.grey,
                                  ),
                                  const SizedBox(width: 8),
                                  Expanded(
                                    child: Text(
                                      widget.appState.publicKey!,
                                      style: const TextStyle(
                                        fontFamily: 'monospace',
                                        fontSize: 10,
                                      ),
                                      overflow: TextOverflow.ellipsis,
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ],
                      ),
                    ),
                    const SizedBox(height: 24),
                  ],
                );
              }
              return const SizedBox.shrink();
            },
          ),

          // Selected Wallet Info
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: NearTheme.green.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(12),
              border: Border.all(color: NearTheme.green.withValues(alpha: 0.3)),
            ),
            child: Row(
              children: [
                Text(
                  _selectedWallet.icon,
                  style: const TextStyle(fontSize: 28),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        _selectedWallet.name,
                        style: const TextStyle(
                          fontWeight: FontWeight.w600,
                          fontSize: 16,
                        ),
                      ),
                      Text(
                        isMainnet ? 'Mainnet' : 'Testnet',
                        style: TextStyle(color: NearTheme.grey, fontSize: 13),
                      ),
                    ],
                  ),
                ),
                Icon(Icons.check_circle, color: NearTheme.green),
              ],
            ),
          ),

          const SizedBox(height: 20),

          // MAIN CONNECT BUTTON - Opens WebView
          _GreenButton(
            label: _connecting
                ? 'Opening Wallet...'
                : (widget.appState.isConnected
                      ? 'Reconnect Wallet'
                      : 'Connect with ${_selectedWallet.name}'),
            icon: Icons.account_balance_wallet,
            isLoading: _connecting,
            onPressed: _connecting ? () {} : _connectWallet,
          ),

          const SizedBox(height: 12),

          // Manual connect option
          OutlinedButton.icon(
            onPressed: _showManualConnectDialog,
            icon: const Icon(Icons.edit, size: 18),
            label: const Text('Enter Account ID manually'),
            style: OutlinedButton.styleFrom(
              foregroundColor: NearTheme.black,
              side: BorderSide(color: NearTheme.greyLight),
              padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
            ),
          ),

          const SizedBox(height: 24),

          // Optional contract ID (collapsed by default)
          ExpansionTile(
            title: Text(
              'Advanced Options',
              style: TextStyle(color: NearTheme.grey, fontSize: 13),
            ),
            tilePadding: EdgeInsets.zero,
            children: [
              TextField(
                controller: _contractController,
                decoration: InputDecoration(
                  labelText: 'Contract ID (optional)',
                  hintText: isMainnet ? 'wrap.near' : 'wrap.testnet',
                ),
              ),
            ],
          ),

          const SizedBox(height: 24),

          // Send Section
          Text(
            'SEND NEAR',
            style: TextStyle(
              color: NearTheme.grey,
              fontSize: 12,
              fontWeight: FontWeight.w600,
              letterSpacing: 1,
            ),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _receiverController,
            decoration: const InputDecoration(labelText: 'Receiver Account ID'),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _amountController,
            decoration: const InputDecoration(
              labelText: 'Amount (NEAR)',
              suffixText: 'NEAR',
            ),
            keyboardType: const TextInputType.numberWithOptions(decimal: true),
          ),
          const SizedBox(height: 12),
          _LoadButton(
            label: _sending ? 'Preparing Transaction...' : 'Send Transaction',
            isLoading: _sending,
            onPressed: _sending ? () {} : _sendTransaction,
          ),

          const SizedBox(height: 32),

          // Info Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Icon(Icons.info_outline, size: 18, color: NearTheme.grey),
                      const SizedBox(width: 8),
                      Text(
                        'How it works',
                        style: TextStyle(
                          fontWeight: FontWeight.w600,
                          color: NearTheme.greyDark,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  Text(
                    '1. Select a wallet and tap "Connect Wallet"\n'
                    '2. Complete login in the wallet browser\n'
                    '3. Return to this app (auto or manually)\n'
                    '4. Or use "Enter Account ID manually"',
                    style: TextStyle(
                      color: NearTheme.grey,
                      fontSize: 13,
                      height: 1.5,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// Green Button Widget
class _GreenButton extends StatelessWidget {
  final String label;
  final IconData icon;
  final bool isLoading;
  final VoidCallback onPressed;

  const _GreenButton({
    required this.label,
    required this.icon,
    required this.isLoading,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton.icon(
        onPressed: isLoading ? null : onPressed,
        icon: isLoading
            ? const SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  color: NearTheme.black,
                ),
              )
            : Icon(icon),
        label: Text(label),
        style: ElevatedButton.styleFrom(
          backgroundColor: NearTheme.green,
          foregroundColor: NearTheme.black,
          padding: const EdgeInsets.symmetric(vertical: 16),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        ),
      ),
    );
  }
}

// ============================================================================
// 11. WALLET URL PAGE
// ============================================================================
class WalletUrlPage extends StatefulWidget {
  final AppState appState;
  const WalletUrlPage({super.key, required this.appState});

  @override
  State<WalletUrlPage> createState() => _WalletUrlPageState();
}

class _WalletUrlPageState extends State<WalletUrlPage> {
  String? _signInUrl;
  String? _txUrl;

  MyNearWalletAdapter _createAdapter() {
    final network = widget.appState.network == Network.mainnet
        ? MyNearWalletNetwork.mainnet
        : MyNearWalletNetwork.testnet;

    return MyNearWalletAdapter(
      config: MyNearWalletConfig(
        contractId: AccountId('example.near'),
        successUrl: 'https://myapp.com/success',
        failureUrl: 'https://myapp.com/failure',
        network: network,
      ),
      launchUrl: (uri) async => true,
    );
  }

  void _buildSignInUrl() {
    final adapter = _createAdapter();

    // /login provisions a function-call key — its REAL public key goes in
    // the URL (example value; signIn() generates one for you).
    setState(() {
      _signInUrl = adapter
          .buildSignInUrl(
            contractId: AccountId('example.near'),
            publicKey: PublicKey(
              'ed25519:9C6hybhQ6Aycep9jaUnP6uL9ZYvDjUp1aSkFWPUFJtpj',
            ),
            methodNames: ['ft_transfer', 'ft_transfer_call'],
          )
          .toString();
    });
  }

  Future<void> _launchSignInUrl() async {
    if (_signInUrl == null) return;
    final uri = Uri.parse(_signInUrl!);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }

  void _buildTxUrl() {
    final adapter = _createAdapter();

    // MyNearWallet signs the transaction, but it still needs a complete,
    // Borsh-serializable Transaction: publicKey, nonce and a recent
    // blockHash. (Example values — a real app fetches nonce via
    // viewAccessKey and blockHash via block().)
    final tx = Transaction(
      signerId: AccountId('alice.near'),
      receiverId: AccountId('bob.near'),
      publicKey: PublicKey(
        'ed25519:9C6hybhQ6Aycep9jaUnP6uL9ZYvDjUp1aSkFWPUFJtpj',
      ),
      nonce: BigInt.from(1),
      blockHash: const CryptoHash(
        '244ZQ9cgj3CQ6bWBdytfrJMuMQ1jdXLFGnr4HhvtCTnM',
      ),
      actions: [TransferAction(deposit: NearToken.fromNear(1))],
    );

    setState(() {
      _txUrl = adapter
          .buildTransactionUrl(
            transactions: [tx],
            callbackUrl: 'https://myapp.com/callback',
          )
          .toString();
    });
  }

  Future<void> _launchTxUrl() async {
    if (_txUrl == null) return;
    final uri = Uri.parse(_txUrl!);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }

  @override
  Widget build(BuildContext context) {
    return _BasePage(
      title: 'Wallet URLs',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'MyNearWallet URL building',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 20),

          _LoadButton(
            label: 'Build Sign-In URL',
            isLoading: false,
            onPressed: _buildSignInUrl,
          ),
          if (_signInUrl != null) ...[
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Sign-In URL',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                        fontSize: 13,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      _signInUrl!,
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 11,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        TextButton.icon(
                          onPressed: () {
                            Clipboard.setData(ClipboardData(text: _signInUrl!));
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Copied')),
                            );
                          },
                          icon: const Icon(Icons.copy, size: 16),
                          label: const Text('Copy'),
                        ),
                        const SizedBox(width: 8),
                        TextButton.icon(
                          onPressed: _launchSignInUrl,
                          icon: const Icon(Icons.open_in_new, size: 16),
                          label: const Text('Launch'),
                          style: TextButton.styleFrom(
                            foregroundColor: NearTheme.green,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ],

          const SizedBox(height: 20),

          _LoadButton(
            label: 'Build Transaction URL',
            isLoading: false,
            onPressed: _buildTxUrl,
          ),
          if (_txUrl != null) ...[
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Transaction URL',
                      style: TextStyle(
                        fontWeight: FontWeight.w600,
                        fontSize: 13,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      _txUrl!,
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 11,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        TextButton.icon(
                          onPressed: () {
                            Clipboard.setData(ClipboardData(text: _txUrl!));
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Copied')),
                            );
                          },
                          icon: const Icon(Icons.copy, size: 16),
                          label: const Text('Copy'),
                        ),
                        const SizedBox(width: 8),
                        TextButton.icon(
                          onPressed: _launchTxUrl,
                          icon: const Icon(Icons.open_in_new, size: 16),
                          label: const Text('Launch'),
                          style: TextButton.styleFrom(
                            foregroundColor: NearTheme.green,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

// ============================================================================
// WALLET WEBVIEW PAGE - Embedded browser for wallet authentication
// ============================================================================
class _WalletWebViewPage extends StatefulWidget {
  final String url;
  final String title;
  final String callbackScheme;

  const _WalletWebViewPage({
    required this.url,
    required this.title,
    required this.callbackScheme,
  });

  @override
  State<_WalletWebViewPage> createState() => _WalletWebViewPageState();
}

class _WalletWebViewPageState extends State<_WalletWebViewPage> {
  InAppWebViewController? _controller;
  bool _isLoading = true;
  double _progress = 0;
  bool _checkingAccount = false;
  bool _foundAccount = false;
  bool _popped = false;

  // JavaScript to extract account from MyNearWallet's localStorage
  // Key format: {prefix}_wallet_auth_key with value {accountId, allKeys}
  static const String _extractAccountScript = '''
    (function() {
      try {
        var keys = Object.keys(localStorage);

        // First: Look for any key ending with _wallet_auth_key
        for (var i = 0; i < keys.length; i++) {
          var key = keys[i];
          if (key.endsWith('_wallet_auth_key')) {
            try {
              var data = JSON.parse(localStorage.getItem(key));
              if (data && data.accountId) {
                return JSON.stringify({accountId: data.accountId, allKeys: data.allKeys || null, source: 'wallet_auth_key'});
              }
            } catch(e) {}
          }
        }

        // Second: Look for near-api-js standard keys
        for (var i = 0; i < keys.length; i++) {
          var key = keys[i];
          if (key.startsWith('near-api-js:keystore:')) {
            // Key format: near-api-js:keystore:{accountId}:{network}
            var parts = key.split(':');
            if (parts.length >= 3 && parts[2]) {
              return JSON.stringify({accountId: parts[2], source: 'keystore'});
            }
          }
        }

        // Third: Check URL for account_id parameter
        var url = window.location.href;
        var match = url.match(/account_id=([^&]+)/);
        if (match) {
          return JSON.stringify({accountId: decodeURIComponent(match[1]), source: 'url'});
        }

        // Fourth: Look for account in page DOM
        var selectors = [
          '[data-test-id="currentAccount"]',
          '[data-testid="currentAccount"]',
          '.account-id',
          '.user-account',
          '[class*="accountId"]',
          '[class*="AccountId"]',
          '[class*="account-name"]',
          '.wallet-account-id'
        ];
        for (var j = 0; j < selectors.length; j++) {
          var el = document.querySelector(selectors[j]);
          if (el && el.textContent) {
            var text = el.textContent.trim();
            if (text.includes('.near') || text.includes('.testnet') || /^[0-9a-f]{64}\$/.test(text)) {
              return JSON.stringify({accountId: text, source: 'dom'});
            }
          }
        }

        // Fifth: Search all text content for .near or .testnet pattern
        var bodyText = document.body ? document.body.innerText : '';
        var accountMatch = bodyText.match(/([a-z0-9_-]+\\.(?:near|testnet))/i);
        if (accountMatch) {
          return JSON.stringify({accountId: accountMatch[1], source: 'body'});
        }

        // Sixth: Search for implicit (hex) account IDs in body
        var hexMatch = bodyText.match(/\\b([0-9a-f]{64})\\b/);
        if (hexMatch) {
          return JSON.stringify({accountId: hexMatch[1], source: 'body_hex'});
        }

        return null;
      } catch(e) {
        return JSON.stringify({error: e.toString()});
      }
    })();
  ''';

  Future<void> _checkForAccount() async {
    if (_controller == null || _checkingAccount || _foundAccount) return;

    _checkingAccount = true;
    try {
      final result = await _controller!.evaluateJavascript(
        source: _extractAccountScript,
      );

      if (result != null && result != 'null' && result.toString().isNotEmpty) {
        try {
          final data = jsonDecode(result.toString());
          if (data['accountId'] != null &&
              data['accountId'].toString().isNotEmpty) {
            final accountId = data['accountId'].toString();
            // Validate it looks like a NEAR account (named or implicit hex)
            if (accountId.contains('.near') ||
                accountId.contains('.testnet') ||
                RegExp(r'^[0-9a-f]{64}$').hasMatch(accountId)) {
              _foundAccount = true;
              if (mounted) {
                // Show quick confirmation and return
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('Found account: $accountId'),
                    backgroundColor: NearTheme.green,
                    duration: const Duration(seconds: 1),
                  ),
                );
                await Future.delayed(const Duration(milliseconds: 500));
                if (mounted) {
                  Navigator.pop(context, {
                    'account_id': accountId,
                    'all_keys': data['allKeys']?.toString(),
                  });
                }
              }
              return;
            }
          }
        } catch (_) {}
      }
    } catch (_) {}
    _checkingAccount = false;
  }

  // Periodically check for account after login
  void _startPeriodicCheck() {
    Future.doWhile(() async {
      if (!mounted || _foundAccount) return false;
      await Future.delayed(const Duration(seconds: 2));
      if (!mounted || _foundAccount) return false;
      await _checkForAccount();
      return mounted && !_foundAccount;
    });
  }

  /// Single-entry callback handler - defers pop to next event loop to avoid _debugLocked
  void _handleCallback(String urlString) {
    if (_popped) return;
    _popped = true;
    debugPrint('[WEBVIEW] CALLBACK INTERCEPTED: $urlString');
    final callbackUri = Uri.parse(urlString);
    final params = Map<String, String>.from(callbackUri.queryParameters);
    debugPrint('[WEBVIEW] Params: $params');
    // Schedule pop on next event loop iteration - completely outside the current frame
    Future.delayed(Duration.zero, () {
      if (mounted) {
        Navigator.pop(context, params);
      }
    });
  }

  void _forceCheckNow() async {
    // Force immediate check
    _checkingAccount = false;
    await _checkForAccount();

    if (!_foundAccount && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Account not detected yet. Complete login first.'),
          backgroundColor: Colors.orange,
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: NearTheme.white,
        foregroundColor: NearTheme.black,
        elevation: 0,
        title: Text(
          widget.title,
          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () => Navigator.pop(context, null),
        ),
        actions: [
          // Button to force check for account
          TextButton.icon(
            onPressed: _forceCheckNow,
            icon: const Icon(Icons.refresh, color: NearTheme.green, size: 20),
            label: const Text(
              'Detect Account',
              style: TextStyle(
                color: NearTheme.green,
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ],
        bottom: _isLoading
            ? PreferredSize(
                preferredSize: const Size.fromHeight(3),
                child: LinearProgressIndicator(
                  value: _progress,
                  backgroundColor: NearTheme.greyLight,
                  valueColor: const AlwaysStoppedAnimation<Color>(
                    NearTheme.green,
                  ),
                ),
              )
            : null,
      ),
      body: SafeArea(
        child: Column(
          children: [
            // Info banner
            Container(
              width: double.infinity,
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
              color: NearTheme.green.withValues(alpha: 0.1),
              child: Row(
                children: [
                  Icon(Icons.info_outline, size: 18, color: NearTheme.green),
                  const SizedBox(width: 10),
                  const Expanded(
                    child: Text(
                      'Login to your wallet - account will be detected automatically',
                      style: TextStyle(
                        fontSize: 13,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                ],
              ),
            ),
            // WebView
            Expanded(
              child: InAppWebView(
                initialUrlRequest: URLRequest(url: WebUri(widget.url)),
                initialSettings: InAppWebViewSettings(
                  useShouldOverrideUrlLoading: true,
                  javaScriptEnabled: true,
                  domStorageEnabled: true,
                  databaseEnabled: true,
                  clearCache: false,
                  cacheEnabled: true,
                  resourceCustomSchemes: [widget.callbackScheme],
                ),
                onWebViewCreated: (controller) {
                  _controller = controller;
                },
                onLoadStart: (controller, url) {
                  debugPrint('[WEBVIEW] onLoadStart: $url');
                  setState(() => _isLoading = true);
                },
                onLoadStop: (controller, url) async {
                  debugPrint('[WEBVIEW] onLoadStop: $url');
                  setState(() => _isLoading = false);

                  // Always try to check for account after page loads
                  await Future.delayed(const Duration(milliseconds: 800));
                  await _checkForAccount();

                  // Start periodic checking if not found yet
                  if (!_foundAccount) {
                    _startPeriodicCheck();
                  }
                },
                onProgressChanged: (controller, progress) {
                  setState(() => _progress = progress / 100);
                },
                onUpdateVisitedHistory: (controller, url, isReload) {
                  final urlStr = url?.toString() ?? '';
                  if (urlStr.startsWith('${widget.callbackScheme}://')) {
                    _handleCallback(urlStr);
                  }
                },
                shouldOverrideUrlLoading: (controller, navigationAction) async {
                  final uri = navigationAction.request.url;
                  if (uri != null) {
                    final urlString = uri.toString();
                    debugPrint(
                      '[WEBVIEW] shouldOverrideUrlLoading: $urlString',
                    );

                    if (urlString.startsWith('${widget.callbackScheme}://')) {
                      _handleCallback(urlString);
                      return NavigationActionPolicy.CANCEL;
                    }
                  }
                  return NavigationActionPolicy.ALLOW;
                },
                onReceivedError: (controller, request, error) {
                  final url = request.url.toString();
                  debugPrint(
                    '[WEBVIEW] onReceivedError: $url - ${error.description}',
                  );
                  if (url.startsWith('${widget.callbackScheme}://')) {
                    _handleCallback(url);
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Sign in with NEAR (NEP-413 → better-near-auth)
// ─────────────────────────────────────────────────────────────────────────────

class SignInWithNearPage extends StatefulWidget {
  final AppState appState;
  const SignInWithNearPage({super.key, required this.appState});

  @override
  State<SignInWithNearPage> createState() => _SignInWithNearPageState();
}

class _SiwnStep {
  _SiwnStep(this.label);
  final String label;
  String? detail;
  bool? ok; // null = running
}

class _SignInWithNearPageState extends State<SignInWithNearPage> {
  final _accountCtrl = TextEditingController();
  final _secretCtrl = TextEditingController();
  final _apiCtrl = TextEditingController(
    text: 'https://nearbuilders.org/api/auth',
  );
  final _recipientCtrl = TextEditingController(text: 'nearbuilders.org');

  bool _busy = false;
  String? _error;
  final List<_SiwnStep> _steps = [];
  Map<String, dynamic>? _session;
  String? _token;

  @override
  void dispose() {
    _accountCtrl.dispose();
    _secretCtrl.dispose();
    _apiCtrl.dispose();
    _recipientCtrl.dispose();
    super.dispose();
  }

  _SiwnStep _step(String label) {
    final s = _SiwnStep(label);
    setState(() => _steps.add(s));
    return s;
  }

  void _finish(_SiwnStep s, bool ok, [String? detail]) {
    setState(() {
      s.ok = ok;
      s.detail = detail;
    });
  }

  Future<void> _createDemoAccount() async {
    setState(() {
      _busy = true;
      _error = null;
    });
    try {
      final kp = await KeyPairEd25519.generate();
      final accountId =
          'near-dart-siwn-${DateTime.now().millisecondsSinceEpoch}.testnet';
      final res = await http.post(
        Uri.parse('https://helper.testnet.near.org/account'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'newAccountId': accountId,
          'newAccountPublicKey': kp.publicKey.value,
        }),
      );
      if (res.statusCode != 200) {
        setState(() {
          _busy = false;
          _error = 'Faucet failed (HTTP ${res.statusCode})';
        });
        return;
      }
      // Wait until the account's key is queryable so /near/nonce (which checks
      // the account exists) won't race the faucet.
      for (var i = 0; i < 15; i++) {
        final check = await widget.appState.client.viewAccessKey(
          accountId: AccountId(accountId),
          publicKey: kp.publicKey,
          blockReference: BlockReference.finality(Finality.final_),
        );
        if (check.isSuccess) break;
        await Future<void>.delayed(const Duration(milliseconds: 1200));
      }
      setState(() {
        _accountCtrl.text = accountId;
        _secretCtrl.text = kp.toString();
        _busy = false;
      });
    } catch (e) {
      setState(() {
        _busy = false;
        _error = '$e';
      });
    }
  }

  Future<void> _signIn() async {
    final accountId = _accountCtrl.text.trim();
    final secret = _secretCtrl.text.trim();
    final api = _apiCtrl.text.trim().replaceAll(RegExp(r'/$'), '');
    final recipient = _recipientCtrl.text.trim();
    if (accountId.isEmpty || secret.isEmpty) {
      setState(
        () => _error =
            'Enter an account and its full-access key '
            '(or create a demo account).',
      );
      return;
    }
    setState(() {
      _busy = true;
      _error = null;
      _steps.clear();
      _session = null;
      _token = null;
    });

    try {
      final keyPair = await KeyPairEd25519.fromString(secret);

      // 1. Challenge nonce from the API
      var s = _step('POST /near/nonce');
      final nonceRes = await http.post(
        Uri.parse('$api/near/nonce'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({'accountId': accountId, 'networkId': 'testnet'}),
      );
      if (nonceRes.statusCode != 200) {
        _finish(s, false, 'HTTP ${nonceRes.statusCode}: ${nonceRes.body}');
        setState(() => _busy = false);
        return;
      }
      final nonceHex = (jsonDecode(nonceRes.body) as Map)['nonce'] as String;
      _finish(s, true, 'nonce ${nonceHex.substring(0, 16)}…');
      final nonce = <int>[
        for (var i = 0; i < nonceHex.length; i += 2)
          int.parse(nonceHex.substring(i, i + 2), radix: 16),
      ];

      // 2. Sign the NEP-413 payload locally
      s = _step('Sign NEP-413 payload (local ed25519)');
      final message = 'Sign in to $recipient';
      final signed = await signNep413Message(
        payload: Nep413Payload(
          message: message,
          recipient: recipient,
          nonce: nonce,
        ),
        keyPair: keyPair,
        accountId: AccountId(accountId),
      );
      _finish(s, true, 'sig ${signed.signature.substring(0, 16)}…');

      // 3. Verify -> session
      s = _step('POST /near/verify');
      final verifyRes = await http.post(
        Uri.parse('$api/near/verify'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'signedMessage': {
            'accountId': accountId,
            'publicKey': signed.publicKey.value,
            'signature': signed.signature,
          },
          'message': message,
          'recipient': recipient,
          'nonce': nonceHex,
          'accountId': accountId,
        }),
      );
      if (verifyRes.statusCode != 200) {
        _finish(s, false, 'HTTP ${verifyRes.statusCode}: ${verifyRes.body}');
        setState(() => _busy = false);
        return;
      }
      final verify = jsonDecode(verifyRes.body) as Map<String, dynamic>;
      final token = verify['token'] as String?;
      _finish(s, true, 'token ${token?.substring(0, 12)}…');

      // 4. Use the session (cookie) for an authenticated call
      s = _step('GET /get-session (authenticated)');
      final cookie = verifyRes.headers['set-cookie'];
      final sessRes = await http.get(
        Uri.parse('$api/get-session'),
        headers: {if (cookie != null) 'Cookie': cookie},
      );
      if (sessRes.statusCode == 200 && sessRes.body.contains(accountId)) {
        _finish(s, true, 'session active');
        setState(() {
          _token = token;
          _session = jsonDecode(sessRes.body) as Map<String, dynamic>;
        });
      } else {
        _finish(s, false, 'HTTP ${sessRes.statusCode}');
      }
      setState(() => _busy = false);
    } catch (e) {
      setState(() {
        _busy = false;
        _error = '$e';
      });
    }
  }

  Widget _field(
    TextEditingController ctrl,
    String label,
    String hint, {
    bool mono = false,
  }) => TextField(
    controller: ctrl,
    style: TextStyle(fontSize: 13, fontFamily: mono ? 'monospace' : null),
    decoration: InputDecoration(
      labelText: label,
      hintText: hint,
      isDense: true,
      border: const OutlineInputBorder(),
    ),
  );

  @override
  Widget build(BuildContext context) {
    final user = _session?['user'] as Map<String, dynamic>?;
    return _BasePage(
      title: 'Sign in with NEAR',
      appState: widget.appState,
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          Text(
            'NEP-413 session auth against a better-near-auth API '
            '(login without a transaction).',
            style: TextStyle(color: NearTheme.grey, fontSize: 13),
          ),
          const SizedBox(height: 16),
          _field(_accountCtrl, 'Account ID', 'alice.testnet'),
          const SizedBox(height: 10),
          _field(
            _secretCtrl,
            'Full-access secret key',
            'ed25519:…',
            mono: true,
          ),
          const SizedBox(height: 10),
          _LoadButton(
            label: 'Create demo account (faucet)',
            isLoading: _busy && _steps.isEmpty,
            onPressed: () {
              if (!_busy) _createDemoAccount();
            },
          ),
          const SizedBox(height: 18),
          _field(
            _apiCtrl,
            'better-near-auth base URL',
            'https://…/api/auth',
            mono: true,
          ),
          const SizedBox(height: 10),
          _field(_recipientCtrl, 'Recipient', 'yourapp.com'),
          const SizedBox(height: 16),
          GlassButton(
            label: 'Sign in with NEAR',
            icon: Icons.login,
            loading: _busy && _steps.isNotEmpty,
            onPressed: _busy ? null : _signIn,
          ),
          const SizedBox(height: 20),
          if (_error != null) _ErrorCard(message: _error!),
          if (_steps.isNotEmpty)
            GlassPanel(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  for (final s in _steps)
                    Padding(
                      padding: const EdgeInsets.symmetric(vertical: 5),
                      child: Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Icon(
                            s.ok == null
                                ? Icons.more_horiz
                                : s.ok!
                                ? Icons.check_circle
                                : Icons.cancel,
                            size: 15,
                            color: s.ok == null
                                ? NearTheme.grey
                                : s.ok!
                                ? Near.mint
                                : Near.danger,
                          ),
                          const SizedBox(width: 8),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  s.label,
                                  style: Near.mono(
                                    12.5,
                                    color: Near.textPrimary,
                                  ),
                                ),
                                if (s.detail != null)
                                  Text(
                                    s.detail!,
                                    style: Near.mono(
                                      11,
                                      color: s.ok == false
                                          ? Near.danger
                                          : NearTheme.grey,
                                    ),
                                  ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ),
                ],
              ),
            ),
          if (user != null) ...[
            const SizedBox(height: 12),
            _ResultCard(
              title: 'Authenticated ✓',
              data: {
                'user_id': user['id'],
                'name': user['name'],
                'email': user['email'],
                'token': _token,
                'expires':
                    (_session?['session']
                        as Map<String, dynamic>?)?['expiresAt'],
              },
            ),
          ],
        ],
      ),
    );
  }
}
0
likes
0
points
392
downloads

Publisher

verified publisher0xj.dev

Weekly Downloads

Complete NEAR Protocol SDK for Flutter/Dart. RPC client, wallet integration, and type-safe primitives.

Homepage
Repository (GitHub)
View/report issues

Topics

#near #blockchain #web3 #crypto #wallet

License

unknown (license)

Dependencies

crypto, cryptography, equatable, http, meta, web_socket_channel

More

Packages that depend on near_dart