511 lines
14 KiB
Dart
511 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
import 'package:lucide_icons/lucide_icons.dart';
|
|
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
|
import '../../core/constants/colors.dart';
|
|
import '../../core/constants/dimensions.dart';
|
|
import '../../widgets/common/gradient_background.dart';
|
|
import '../../widgets/common/glass_card.dart';
|
|
import '../../widgets/effects/cashu_confetti.dart';
|
|
import '../../providers/wallet_provider.dart';
|
|
|
|
/// Estados del proceso de mint
|
|
enum MintStatus { loading, unpaid, paid, issued, error }
|
|
|
|
/// Pantalla para mostrar el invoice generado y esperar el pago
|
|
class InvoiceScreen extends StatefulWidget {
|
|
final int amount;
|
|
final String unit;
|
|
final String? description;
|
|
|
|
const InvoiceScreen({
|
|
super.key,
|
|
required this.amount,
|
|
required this.unit,
|
|
this.description,
|
|
});
|
|
|
|
@override
|
|
State<InvoiceScreen> createState() => _InvoiceScreenState();
|
|
}
|
|
|
|
class _InvoiceScreenState extends State<InvoiceScreen> {
|
|
MintStatus _status = MintStatus.loading;
|
|
String? _invoice;
|
|
String? _errorMessage;
|
|
StreamSubscription<MintQuote>? _mintSubscription;
|
|
final CashuConfettiController _confettiController = CashuConfettiController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_startMintProcess();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_mintSubscription?.cancel();
|
|
_confettiController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startMintProcess() {
|
|
final walletProvider = context.read<WalletProvider>();
|
|
|
|
try {
|
|
final mintStream = walletProvider.mintTokens(
|
|
BigInt.from(widget.amount),
|
|
widget.description,
|
|
);
|
|
|
|
_mintSubscription = mintStream.listen(
|
|
(quote) {
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
switch (quote.state) {
|
|
case MintQuoteState.unpaid:
|
|
_status = MintStatus.unpaid;
|
|
_invoice = quote.request;
|
|
break;
|
|
|
|
case MintQuoteState.paid:
|
|
_status = MintStatus.paid;
|
|
break;
|
|
|
|
case MintQuoteState.issued:
|
|
_status = MintStatus.issued;
|
|
_onMintCompleted();
|
|
break;
|
|
|
|
default:
|
|
// Cualquier otro estado se trata como error
|
|
_status = MintStatus.error;
|
|
_errorMessage = 'Estado desconocido';
|
|
}
|
|
});
|
|
},
|
|
onError: (error) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_status = MintStatus.error;
|
|
_errorMessage = error.toString();
|
|
});
|
|
},
|
|
);
|
|
} catch (e) {
|
|
setState(() {
|
|
_status = MintStatus.error;
|
|
_errorMessage = e.toString();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onMintCompleted() {
|
|
// Disparar confetti inmediatamente
|
|
_confettiController.fire();
|
|
|
|
// Esperar a que termine el confetti antes de navegar
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
if (mounted) {
|
|
// Volver al home con mensaje de éxito
|
|
Navigator.popUntil(context, (route) => route.isFirst);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'+${widget.amount} ${widget.unit} depositados',
|
|
),
|
|
backgroundColor: AppColors.success,
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CashuConfetti(
|
|
controller: _confettiController,
|
|
child: GradientBackground(
|
|
child: PopScope(
|
|
canPop: _status == MintStatus.issued || _status == MintStatus.error,
|
|
child: Scaffold(
|
|
backgroundColor: Colors.transparent,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
leading: (_status == MintStatus.issued || _status == MintStatus.error)
|
|
? IconButton(
|
|
icon: const Icon(
|
|
LucideIcons.arrowLeft,
|
|
color: Colors.white,
|
|
),
|
|
onPressed: () => Navigator.pop(context),
|
|
)
|
|
: null,
|
|
title: const Text(
|
|
'Pagar invoice',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
actions: [
|
|
if (_status == MintStatus.unpaid)
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text(
|
|
'Cancelar',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
|
child: _buildContent(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildContent() {
|
|
switch (_status) {
|
|
case MintStatus.loading:
|
|
return _buildLoading();
|
|
case MintStatus.unpaid:
|
|
case MintStatus.paid:
|
|
case MintStatus.issued:
|
|
return _buildInvoiceView();
|
|
case MintStatus.error:
|
|
return _buildError();
|
|
}
|
|
}
|
|
|
|
Widget _buildLoading() {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
color: AppColors.primaryAction,
|
|
),
|
|
SizedBox(height: AppDimensions.paddingMedium),
|
|
Text(
|
|
'Generando invoice...',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInvoiceView() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Título con monto
|
|
_buildHeader(),
|
|
|
|
const SizedBox(height: AppDimensions.paddingLarge),
|
|
|
|
// QR del invoice
|
|
if (_invoice != null) _buildQRSection(),
|
|
|
|
const SizedBox(height: AppDimensions.paddingLarge),
|
|
|
|
// Invoice truncado
|
|
if (_invoice != null) _buildInvoiceText(),
|
|
|
|
const SizedBox(height: AppDimensions.paddingMedium),
|
|
|
|
// Botón copiar
|
|
if (_invoice != null && _status == MintStatus.unpaid) _buildCopyButton(),
|
|
|
|
const Spacer(),
|
|
|
|
// Estado del invoice
|
|
_buildStatus(),
|
|
|
|
const SizedBox(height: AppDimensions.paddingMedium),
|
|
|
|
// Descripción (si existe)
|
|
if (widget.description != null && widget.description!.isNotEmpty)
|
|
_buildDescription(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildError() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 80,
|
|
height: 80,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.error.withValues(alpha: 0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
LucideIcons.alertCircle,
|
|
color: AppColors.error,
|
|
size: 40,
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimensions.paddingMedium),
|
|
const Text(
|
|
'Error',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimensions.paddingSmall),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Text(
|
|
_errorMessage ?? 'Error desconocido',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
const SizedBox(height: AppDimensions.paddingLarge),
|
|
GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Text(
|
|
'Volver',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
'Depositar ${widget.amount} ${widget.unit}',
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQRSection() {
|
|
return GlassCard(
|
|
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
|
|
child: Center(
|
|
child: QrImageView(
|
|
data: _invoice!,
|
|
version: QrVersions.auto,
|
|
size: 200,
|
|
backgroundColor: Colors.white,
|
|
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInvoiceText() {
|
|
return GlassCard(
|
|
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
|
|
child: SelectableText(
|
|
_invoice!,
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: Colors.white,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCopyButton() {
|
|
return GestureDetector(
|
|
onTap: _copyInvoice,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppDimensions.paddingMedium,
|
|
vertical: AppDimensions.paddingSmall + 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Colors.white.withValues(alpha: 0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(LucideIcons.copy, color: AppColors.textSecondary, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Copiar invoice',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatus() {
|
|
IconData icon;
|
|
String text;
|
|
Color color;
|
|
|
|
switch (_status) {
|
|
case MintStatus.loading:
|
|
icon = LucideIcons.clock;
|
|
text = 'Generando...';
|
|
color = AppColors.textSecondary;
|
|
break;
|
|
case MintStatus.unpaid:
|
|
icon = LucideIcons.clock;
|
|
text = 'Esperando pago...';
|
|
color = AppColors.textSecondary;
|
|
break;
|
|
case MintStatus.paid:
|
|
icon = LucideIcons.checkCircle;
|
|
text = 'Pago recibido';
|
|
color = AppColors.success;
|
|
break;
|
|
case MintStatus.issued:
|
|
icon = LucideIcons.checkCircle2;
|
|
text = 'Tokens emitidos!';
|
|
color = AppColors.success;
|
|
break;
|
|
case MintStatus.error:
|
|
icon = LucideIcons.xCircle;
|
|
text = 'Error';
|
|
color = AppColors.error;
|
|
break;
|
|
}
|
|
|
|
return GlassCard(
|
|
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (_status == MintStatus.unpaid || _status == MintStatus.loading)
|
|
SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
),
|
|
)
|
|
else
|
|
Icon(icon, color: color, size: 24),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDescription() {
|
|
return GlassCard(
|
|
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Descripcion:',
|
|
style: TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 12,
|
|
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.description!,
|
|
style: const TextStyle(
|
|
fontFamily: 'Inter',
|
|
fontSize: 14,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _copyInvoice() async {
|
|
if (_invoice == null) return;
|
|
await Clipboard.setData(ClipboardData(text: _invoice!));
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Invoice copiado al portapapeles'),
|
|
backgroundColor: AppColors.success,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|