Skip to content
Snippets Groups Projects
pay_helper.dart 12.6 KiB
Newer Older
poka's avatar
poka committed
// ignore_for_file: use_build_context_synchronously

vjrj's avatar
vjrj committed
import 'dart:async';

vjrj's avatar
vjrj committed
import 'package:durt/durt.dart';
vjrj's avatar
vjrj committed
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
poka's avatar
poka committed
import '../data/models/cesium_card.dart';
import 'package:ndef/utilities.dart';
import 'qr_manager.dart';
vjrj's avatar
vjrj committed
import 'package:tuple/tuple.dart';
vjrj's avatar
vjrj committed

import '../../../data/models/contact.dart';
import '../../../data/models/node_type.dart';
import '../../../data/models/payment_cubit.dart';
import '../../../data/models/transaction.dart';
import '../../../data/models/transaction_type.dart';
import '../../../g1/api.dart';
import '../../../shared_prefs_helper.dart';
import '../data/models/app_cubit.dart';
import '../data/models/bottom_nav_cubit.dart';
vjrj's avatar
vjrj committed
import '../data/models/multi_wallet_transaction_cubit.dart';
import '../data/models/node.dart';
import '../data/models/node_manager.dart';
vjrj's avatar
vjrj committed
import '../data/models/payment_state.dart';
vjrj's avatar
vjrj committed
import '../data/models/utxo.dart';
import '../data/models/utxo_cubit.dart';
import '../g1/currency.dart';
import '../g1/astroid_helper.dart';
import '../g1/g1_helper.dart';
vjrj's avatar
vjrj committed
import 'contacts_cache.dart';
import 'logger.dart';
import 'ui_helpers.dart';
vjrj's avatar
vjrj committed
import 'widgets/connectivity_widget_wrapper_wrapper.dart';
vjrj's avatar
vjrj committed
import 'widgets/fifth_screen/import_dialog.dart';
Future<bool> payWithRetry(
vjrj's avatar
vjrj committed
    {required BuildContext context,
vjrj's avatar
vjrj committed
    required List<Contact> recipients,
vjrj's avatar
vjrj committed
    required double amount,
    required String comment,
    bool isRetry = false,
    required bool isG1,
vjrj's avatar
vjrj committed
    required double currentUd,
    bool useBMA = false}) async {
  assert(amount > 0);
vjrj's avatar
vjrj committed
  bool hasPass = false;
vjrj's avatar
vjrj committed
  final bool isToMultiple = recipients.length > 1;
vjrj's avatar
vjrj committed
  if (!SharedPreferencesHelper().isG1nkgoCard() &&
      !SharedPreferencesHelper().hasVolatile()) {
    hasPass = await showImportCesiumWalletDialog(
            context,
            SharedPreferencesHelper().getPubKey(),
            context.read<BottomNavCubit>().currentIndex) ??
        false;
vjrj's avatar
vjrj committed
  } else {
vjrj's avatar
vjrj committed
    hasPass = true;
  }
  if (hasPass) {
    if (context.mounted) {
      final MultiWalletTransactionCubit txCubit =
          context.read<MultiWalletTransactionCubit>();
      final PaymentCubit paymentCubit = context.read<PaymentCubit>();
      final AppCubit appCubit = context.read<AppCubit>();
      paymentCubit.sending();
      final String fromPubKey = SharedPreferencesHelper().getPubKey();
vjrj's avatar
vjrj committed

      final bool? confirmed = await _confirmSend(context, amount.toString(),
          fromPubKey, recipients, isRetry, appCubit.currency, isToMultiple);
vjrj's avatar
vjrj committed
      final Contact fromContact = await ContactsCache().getContact(fromPubKey);
vjrj's avatar
vjrj committed
      final CesiumWallet wallet = await SharedPreferencesHelper().getWallet();
      if (!context.mounted) {
        return false;
      }
      final UtxoCubit utxoCubit = context.read<UtxoCubit>();
vjrj's avatar
vjrj committed
      final double convertedAmount = toG1(amount, isG1, currentUd);
      if (confirmed == null || !confirmed) {
        paymentCubit.sentFailed();
      } else {
        final Transaction tx = Transaction(
            type: TransactionType.pending,
            from: fromContact,
vjrj's avatar
vjrj committed
            to: recipients[0],
            recipients: recipients,
            recipientsAmounts: List<double>.filled(recipients.length, amount),
            amount: -toCG1(convertedAmount).toDouble() * recipients.length,
vjrj's avatar
vjrj committed
            comment: comment,
            time: DateTime.now());
        final bool isConnected =
            await ConnectivityWidgetWrapperWrapper.isConnected;
        logger('isConnected: $isConnected');
        if (isConnected != null && !isConnected && !isRetry) {
          paymentCubit.sent();
          if (!context.mounted) {
            return true;
          }
vjrj's avatar
vjrj committed
          showAlertDialog(context, tr('payment_waiting_internet_title'),
              tr('payment_waiting_internet_desc_beta'));
vjrj's avatar
vjrj committed
          final Transaction pending =
              tx.copyWith(type: TransactionType.waitingNetwork);
vjrj's avatar
vjrj committed
          txCubit.addPendingTransaction(pending);
vjrj's avatar
vjrj committed
          context.read<BottomNavCubit>().updateIndex(3);
          return true;
vjrj's avatar
vjrj committed
        } else {
vjrj's avatar
vjrj committed
          // PAY!
          PayResult result;
          if (!useBMA) {
            result = await payWithGVA(
                to: recipients.map((Contact c) => c.pubKey).toList(),
                comment: comment,
                amount: convertedAmount);
          } else {
            await utxoCubit.fetchUtxos(fromPubKey);
            final List<Utxo>? utxos = utxoCubit.consume(convertedAmount);
            final Tuple2<Map<String, dynamic>?, Node> currentBlock =
                await getCurrentBlockGVA();

            if (currentBlock != null && utxos != null) {
              result = await payWithBMA(
                  destPub: recipients[0].pubKey,
                  blockHash: '${currentBlock.item1!['hash']}',
                  blockNumber: '${currentBlock.item1!['number']}',
                  comment: comment,
                  wallet: wallet,
                  utxos: utxos,
                  amount: convertedAmount);
            } else {
              final Node triedNode = currentBlock.item2;
              result = PayResult(
                  message: 'Error retrieving payment data', node: triedNode);
            }
          }
vjrj's avatar
vjrj committed
          final Transaction pending = tx.copyWith(
              debugInfo:
vjrj's avatar
vjrj committed
                  'Node used: ${result != null && result.node != null ? result.node!.url : 'unknown'}');
vjrj's avatar
vjrj committed
          if (result.message == 'success') {
            paymentCubit.sent();
            if (!context.mounted) {
              return true;
            }
vjrj's avatar
vjrj committed
            showAlertDialog(context, tr('payment_successful'),
                tr('payment_successful_desc'));
vjrj's avatar
vjrj committed
            if (!isRetry) {
              // Add here the transaction to the pending list (so we can check it the tx is confirmed)
              txCubit.addPendingTransaction(pending);
            } else {
              // Update the previously failed tx with an update time and type pending
vjrj's avatar
vjrj committed
              txCubit.updatePendingTransaction(
                  pending.copyWith(type: TransactionType.pending));
vjrj's avatar
vjrj committed
            context.read<BottomNavCubit>().updateIndex(3);
            return true;
vjrj's avatar
vjrj committed
          } else {
            paymentCubit.pendingPayment();
            if (!context.mounted) {
              return false;
vjrj's avatar
vjrj committed
            final bool failedWithoutBalance =
                result.message == 'insufficient balance' ||
                    result.message == 'Insufficient balance in your wallet';
vjrj's avatar
vjrj committed
            showPayError(
                context: context,
vjrj's avatar
vjrj committed
                desc: tr('payment_error_desc',
                    namedArgs: <String, String>{'error': tr(result.message)}),
                increaseErrors: !failedWithoutBalance,
vjrj's avatar
vjrj committed
                node: result.node);
vjrj's avatar
vjrj committed
            if (!isRetry) {
              txCubit.insertPendingTransaction(
                  pending.copyWith(type: TransactionType.failed));
              context.read<BottomNavCubit>().updateIndex(3);
            } else {
              // Update the previously failed tx with an update time and type pending
              txCubit.updatePendingTransaction(
                  pending.copyWith(type: TransactionType.failed));
            }
            return false;
vjrj's avatar
vjrj committed
        }
vjrj's avatar
vjrj committed
  } else {
    if (context.mounted) {
      showPayError(
          context: context,
          desc: tr('payment_error_no_pass'),
          increaseErrors: false);
    return false;
  return true;
vjrj's avatar
vjrj committed
}

bool weHaveBalance(BuildContext context, double amount) {
  final double balance = getBalance(context);
vjrj's avatar
vjrj committed
  final bool weHave = balance >= toCG1(amount);
vjrj's avatar
vjrj committed
  return weHave;
}

double getBalance(BuildContext context) =>
    context.read<MultiWalletTransactionCubit>().balance();
vjrj's avatar
vjrj committed
Future<bool?> _confirmSend(
    BuildContext context,
    String amount,
    String fromPubKey,
    List<Contact> recipients,
    bool isRetry,
    Currency currency,
    bool isPayToMultiple) async {
vjrj's avatar
vjrj committed
  return showDialog<bool>(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text(tr('please_confirm_sent')),
vjrj's avatar
vjrj committed
        content: isPayToMultiple
            ? Text(tr('please_confirm_sent_multi_desc',
                namedArgs: <String, String>{
                    'amount': amount,
                    'currency': currency.name(),
                    'people': recipients.length.toString()
                  }))
            : Text(tr(
                isRetry
                    ? 'please_confirm_retry_sent_desc'
                    : 'please_confirm_sent_desc',
                namedArgs: <String, String>{
                    'amount': amount,
                    'to': humanizeContact(fromPubKey, recipients[0], true),
                    'currency': currency.name()
                  })),
vjrj's avatar
vjrj committed
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: Text(tr('cancel')),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: Text(tr('yes_sent')),
          ),
        ],
      );
    },
  );
}

void showPayError(
    {required BuildContext context,
    required String desc,
    required bool increaseErrors,
    Node? node}) {
vjrj's avatar
vjrj committed
  showAlertDialog(context, tr('payment_error'), desc);
vjrj's avatar
vjrj committed
  context.read<PaymentCubit>().sentFailed();
  if (node != null && increaseErrors) {
    NodeManager().increaseNodeErrors(NodeType.gva, node);
  }

const Duration paymentTimeRange = Duration(minutes: 60);
vjrj's avatar
vjrj committed

Future<void> onKeyScanned(BuildContext context, String scannedKey) async {
  final PaymentState? pay = parseScannedUri(scannedKey);
  final PaymentCubit paymentCubit = context.read<PaymentCubit>();
  if (pay != null) {
    logger('Scanned $pay');
    final String result = extractPublicKey(pay.contacts[0].pubKey);
vjrj's avatar
vjrj committed

    final Contact contact = await ContactsCache().getContact(result);
vjrj's avatar
vjrj committed
    final double? currentAmount = paymentCubit.state.amount;
    paymentCubit.selectUser(contact);
    if (pay.amount != null) {
      paymentCubit.selectKeyAmount(contact, pay.amount);
    } else {
      paymentCubit.selectKeyAmount(contact, currentAmount);
    }
    if (pay.comment != null) {
      paymentCubit.setComment(pay.comment);
    }
  } else {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(tr('qr_invalid_payment'))));
  }
}

Future<void> importAstroID(BuildContext context) async {
  try {
    // Scanner le QR code et extraire la valeur DISCO
    final String? disco = await QrManager.qrScan(context);
    if (disco == null) {
      return;
    }

    // Demander à l'utilisateur de saisir le mot de passe unique ($UNIQID)
poka's avatar
poka committed
    final String? password = await showDialog<String>(
poka's avatar
poka committed
      builder: (BuildContext context) {
        String passwordLocal = '';
        return AlertDialog(
          title: const Text('Saisir le mot de passe'),
          content: TextField(
            decoration: const InputDecoration(hintText: 'Mot de passe unique'),
            onChanged: (String value) {
              // Handle password input
              passwordLocal = value;
            },
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () => Navigator.of(context).pop(null),
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                // Handle password submission
                Navigator.of(context).pop(passwordLocal);
              },
              child: const Text('OK'),
            ),
          ],
        );
      },
    );
    if (password == null || password.isEmpty) {
      return;
    }

    // Déchiffrer l'AstroID
    final Tuple2<String, String>? secrets =
        await decryptAstroID(disco, password);
    if (secrets == null) {
      showAlertDialog(context, 'Erreur',
          'Impossible de déchiffrer AstroID. Vérifiez le mot de passe.');
      return;
    }

    // Initialiser un nouveau portefeuille CesiumWallet avec les secrets déchiffrés
poka's avatar
poka committed
    final CesiumWallet wallet = CesiumWallet(secrets.item1, secrets.item2);
poka's avatar
poka committed
    // Add the AstroID wallet to the storage
    SharedPreferencesHelper().importAstroIDWallet(disco, password);
    final CesiumCard card = SharedPreferencesHelper().buildCesiumCard(
        seed: wallet.seed.toHexString(), pubKey: wallet.pubkey);
poka's avatar
poka committed
    // Select the AstroID wallet as the current wallet
    SharedPreferencesHelper().selectCurrentWallet(card);

    // Rediriger vers l'écran principal
    Navigator.of(context).pushReplacementNamed('/');
  } catch (e, stacktrace) {
    logger('Erreur importation AstroID: $e');
    logger(stacktrace.toString());
    showAlertDialog(context, 'Erreur',
        'Une erreur est survenue lors de importation de AstroID.');
  }
}