git / moonbite-network / mobile/lib/screens/onboarding_screen.dart · raw

import 'package:flutter/material.dart';import 'package:provider/provider.dart';import '../wallet/moonbite_network.dart';import '../wallet/secure_clipboard.dart';import '../wallet/wallet_controller.dart';/// First-run flow: create a new wallet (shows the recovery phrase to back up)/// or import an existing BIP39 phrase. Defaults to testnet for safety.class OnboardingScreen extends StatelessWidget {  const OnboardingScreen({super.key});  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: const Text('MoonBite Wallet')),      body: Padding(        padding: const EdgeInsets.all(24),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            const Spacer(),            const Icon(Icons.account_balance_wallet,                size: 72, color: Color(0xFF1F6FD9)),            const SizedBox(height: 16),            const Text(              'A non-custodial wallet.\nYour keys stay on this device.',              textAlign: TextAlign.center,              style: TextStyle(fontSize: 16, color: Colors.white70),            ),            const Spacer(),            FilledButton.icon(              icon: const Icon(Icons.add),              label: const Text('Create new wallet'),              onPressed: () => _createWallet(context),            ),            const SizedBox(height: 12),            OutlinedButton.icon(              icon: const Icon(Icons.download),              label: const Text('Import recovery phrase'),              onPressed: () => Navigator.of(context).push(                MaterialPageRoute(builder: (_) => const _ImportScreen()),              ),            ),            const SizedBox(height: 24),            const Text(              'Network: testnet (safe mode)',              textAlign: TextAlign.center,              style: TextStyle(fontSize: 12, color: Colors.white38),            ),          ],        ),      ),    );  }  Future<void> _createWallet(BuildContext context) async {    final controller = context.read<WalletController>();    final phrase =        await controller.createNew(network: MoonBiteNetwork.testnet);    if (!context.mounted) return;    await showDialog<void>(      context: context,      barrierDismissible: false,      builder: (_) => _BackupDialog(phrase: phrase),    );  }}class _BackupDialog extends StatelessWidget {  final String phrase;  const _BackupDialog({required this.phrase});  @override  Widget build(BuildContext context) {    final words = phrase.split(' ');    return AlertDialog(      title: const Text('Back up your recovery phrase'),      content: SingleChildScrollView(        child: Column(          mainAxisSize: MainAxisSize.min,          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            const Text(              'Write these 12 words down in order. Anyone with them controls '              'your coins. They are never sent anywhere.',              style: TextStyle(color: Colors.white70, fontSize: 13),            ),            const SizedBox(height: 16),            Wrap(              spacing: 8,              runSpacing: 8,              children: [                for (var i = 0; i < words.length; i++)                  Chip(label: Text('${i + 1}. ${words[i]}')),              ],            ),            const SizedBox(height: 12),            Builder(              builder: (context) => TextButton.icon(                icon: const Icon(Icons.copy, size: 18),                label: const Text('Copy phrase (auto-clears in 30s)'),                onPressed: () {                  SecureClipboard().copyEphemeral(phrase);                  ScaffoldMessenger.of(context).showSnackBar(                    const SnackBar(                      content: Text(                        'Phrase copied. It will be cleared from the clipboard '                        'in 30 seconds — paste it somewhere safe now.',                      ),                    ),                  );                },              ),            ),          ],        ),      ),      actions: [        FilledButton(          onPressed: () => Navigator.of(context).pop(),          child: const Text("I've saved it"),        ),      ],    );  }}class _ImportScreen extends StatefulWidget {  const _ImportScreen();  @override  State<_ImportScreen> createState() => _ImportScreenState();}class _ImportScreenState extends State<_ImportScreen> {  final _controller = TextEditingController();  String? _error;  bool _busy = false;  @override  void dispose() {    _controller.dispose();    super.dispose();  }  Future<void> _import() async {    setState(() {      _busy = true;      _error = null;    });    try {      await context          .read<WalletController>()          .importExisting(_controller.text, network: MoonBiteNetwork.testnet);      if (mounted) {        Navigator.of(context)          ..pop()          ..pop();      }    } catch (e) {      setState(() => _error = 'Invalid recovery phrase');    } finally {      if (mounted) setState(() => _busy = false);    }  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: const Text('Import wallet')),      body: Padding(        padding: const EdgeInsets.all(24),        child: Column(          crossAxisAlignment: CrossAxisAlignment.stretch,          children: [            const Text('Enter your 12 or 24-word recovery phrase.',                style: TextStyle(color: Colors.white70)),            const SizedBox(height: 16),            TextField(              controller: _controller,              maxLines: 4,              autocorrect: false,              enableSuggestions: false,              decoration: InputDecoration(                border: const OutlineInputBorder(),                hintText: 'word1 word2 word3 ...',                errorText: _error,              ),            ),            const SizedBox(height: 16),            FilledButton(              onPressed: _busy ? null : _import,              child: _busy                  ? const SizedBox(                      height: 18,                      width: 18,                      child: CircularProgressIndicator(strokeWidth: 2))                  : const Text('Import'),            ),          ],        ),      ),    );  }}