Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
feat: get transactions from pfi (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanwlee authored May 14, 2024
1 parent a5fa32f commit 4463f8d
Show file tree
Hide file tree
Showing 16 changed files with 340 additions and 150 deletions.
89 changes: 69 additions & 20 deletions lib/features/home/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import 'package:didpay/features/home/transaction_details_page.dart';
import 'package:didpay/features/payin/deposit_page.dart';
import 'package:didpay/features/payout/withdraw_page.dart';
import 'package:didpay/features/tbdex/rfq_state.dart';
import 'package:didpay/features/tbdex/tbdex_providers.dart';
import 'package:didpay/features/tbdex/transactions_notifier.dart';
import 'package:didpay/l10n/app_localizations.dart';
import 'package:didpay/shared/theme/grid.dart';
import 'package:didpay/shared/utils/currency_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tbdex/tbdex.dart';

class HomePage extends HookConsumerWidget {
const HomePage({super.key});
Expand All @@ -19,14 +23,25 @@ class HomePage extends HookConsumerWidget {
// TODO(ethan-tbd): get balance from pfi, https://github.com/TBD54566975/didpay/issues/109
final accountBalance = CurrencyUtil.formatFromDouble(0);

TransactionsAsyncNotifier getTransactionsNotifier() =>
ref.read(transactionsProvider.notifier);

useEffect(
() {
Future.delayed(Duration.zero, () => getTransactionsNotifier().fetch());
return null;
},
[],
);

return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAccountBalance(context, accountBalance),
Expanded(
child: _buildActivityList(context, txns),
child: _buildActivity(context, getTransactionsNotifier(), txns),
),
],
),
Expand Down Expand Up @@ -126,7 +141,11 @@ class HomePage extends HookConsumerWidget {
),
);

Widget _buildActivityList(BuildContext context, List<Transaction> txns) =>
Widget _buildActivity(
BuildContext context,
TransactionsAsyncNotifier notifier,
AsyncValue<List<Exchange>?> exchangesStatus,
) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -141,18 +160,25 @@ class HomePage extends HookConsumerWidget {
),
),
Expanded(
child: txns.isEmpty
? _buildEmptyState(context)
: _buildTransactionsList(context, txns),
child: exchangesStatus.when(
data: (exchange) => exchange == null || exchange.isEmpty
? _buildEmptyState(context)
: RefreshIndicator(
onRefresh: () async => notifier.fetch(),
child: _buildTransactionsList(context, exchange),
),
error: (error, stackTrace) => _buildErrorState(context),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
],
);

// TODO(ethan-tbd): update empty state, https://github.com/TBD54566975/didpay/issues/125
Widget _buildEmptyState(BuildContext context) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: Grid.xxs),
const SizedBox(height: Grid.xs),
Text(
Loc.of(context).noTransactionsYet,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
Expand All @@ -176,33 +202,53 @@ class HomePage extends HookConsumerWidget {
),
);

Widget _buildTransactionsList(BuildContext context, List<Transaction> txns) =>
// TODO(ethan-tbd): update error state, https://github.com/TBD54566975/didpay/issues/125
Widget _buildErrorState(BuildContext context) => Center(
child: Column(
children: [
const SizedBox(height: Grid.xs),
Text(
Loc.of(context).unableToRetrieveTxns,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: Grid.xxs),
],
),
);

Widget _buildTransactionsList(
BuildContext context,
List<Exchange> exchanges,
) =>
ListView(
children: txns.map((txn) {
children: exchanges.map((exchange) {
final transaction = Transaction.fromExchange(exchange);
final payoutAmount = CurrencyUtil.formatFromDouble(
txn.payoutAmount,
currency: txn.payoutCurrency.toUpperCase(),
transaction.payoutAmount,
currency: transaction.payoutCurrency.toUpperCase(),
);
final payinAmount = CurrencyUtil.formatFromDouble(
txn.payinAmount,
currency: txn.payinCurrency.toUpperCase(),
transaction.payinAmount,
currency: transaction.payinCurrency.toUpperCase(),
);

return ListTile(
title: Text(
'${txn.type}',
'${transaction.type}',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
'${txn.status}',
_getSubtitle(transaction),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w300,
),
),
trailing: Text(
'${txn.type == TransactionType.deposit ? payoutAmount : payinAmount} USDC',
'${transaction.type == TransactionType.deposit ? payoutAmount : payinAmount} USDC',
),
leading: Container(
width: Grid.md,
Expand All @@ -212,21 +258,24 @@ class HomePage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(Grid.xxs),
),
child: Center(
child: txn.type == TransactionType.deposit
? const Icon(Icons.south_west)
: const Icon(Icons.north_east),
child: Transaction.getIcon(transaction.type),
),
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) {
return TransactionDetailsPage(
txn: txn,
txn: transaction,
);
},
),
),
);
}).toList(),
);

String _getSubtitle(Transaction transaction) =>
transaction.type == TransactionType.deposit
? '${transaction.payinCurrency} → account balance'
: 'Account balance → ${transaction.payoutCurrency}';
}
113 changes: 65 additions & 48 deletions lib/features/home/transaction.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:didpay/shared/theme/grid.dart';
import 'package:flutter/material.dart';
import 'package:tbdex/tbdex.dart';

// TODO(ethan-tbd): remove this file, https://github.com/TBD54566975/didpay/issues/136
class Transaction {
final double payinAmount;
final double payoutAmount;
final String payinCurrency;
final String payoutCurrency;
final DateTime createdAt;
final TransactionStatus status;
final TransactionType type;

Expand All @@ -14,57 +16,72 @@ class Transaction {
required this.payoutAmount,
required this.payinCurrency,
required this.payoutCurrency,
required this.createdAt,
required this.status,
required this.type,
});
}

final _defaultList = [
Transaction(
payinAmount: 25,
payoutAmount: 1.47,
payinCurrency: 'MXN',
payoutCurrency: 'USDC',
status: TransactionStatus.pending,
type: TransactionType.deposit,
),
Transaction(
payinAmount: 1,
payoutAmount: 17,
payinCurrency: 'USDC',
payoutCurrency: 'MXN',
status: TransactionStatus.pending,
type: TransactionType.withdraw,
),
Transaction(
payinAmount: 0.00085,
payoutAmount: 35.42,
payinCurrency: 'BTC',
payoutCurrency: 'USDC',
status: TransactionStatus.completed,
type: TransactionType.deposit,
),
Transaction(
payinAmount: 33,
payoutAmount: 0.000792,
payinCurrency: 'USDC',
payoutCurrency: 'BTC',
status: TransactionStatus.completed,
type: TransactionType.withdraw,
),
Transaction(
payinAmount: 1,
payoutAmount: 1,
payinCurrency: 'USDC',
payoutCurrency: 'USD',
status: TransactionStatus.failed,
type: TransactionType.withdraw,
),
];
factory Transaction.fromExchange(Exchange exchange) {
var payinAmount = '0';
var payoutAmount = '0';
var payinCurrency = 'USD';
var payoutCurrency = 'MXN';
var latestCreatedAt = DateTime.fromMillisecondsSinceEpoch(0);
var status = TransactionStatus.pending;
var type = TransactionType.send;

for (final msg in exchange) {
switch (msg.metadata.kind) {
case MessageKind.rfq:
payinAmount = (msg as Rfq).data.payin.amount;
break;
case MessageKind.quote:
payinAmount = (msg as Quote).data.payin.amount;
payoutAmount = msg.data.payout.amount;
payinCurrency = msg.data.payin.currencyCode;
payoutCurrency = msg.data.payout.currencyCode;
break;
case MessageKind.order:
status = TransactionStatus.completed;
break;
// TODO(ethan-tbd): add additional order statuses
case MessageKind.orderstatus:
status = TransactionStatus.completed;
break;
case MessageKind.close:
status = TransactionStatus.failed;
break;
}
}

type = (payinCurrency == 'STORED_BALANCE')
? TransactionType.withdraw
: (payoutCurrency == 'STORED_BALANCE')
? TransactionType.deposit
: type;

final transactionsProvider = StateProvider<List<Transaction>>((ref) {
return _defaultList;
});
return Transaction(
payinAmount: double.parse(payinAmount),
payoutAmount: double.parse(payoutAmount),
payinCurrency: payinCurrency,
payoutCurrency: payoutCurrency,
createdAt: latestCreatedAt,
status: status,
type: type,
);
}

static Icon getIcon(TransactionType type, {double size = Grid.sm}) {
switch (type) {
case TransactionType.deposit:
return Icon(Icons.south_west, size: size);
case TransactionType.withdraw:
return Icon(Icons.north_east, size: size);
case TransactionType.send:
return Icon(Icons.attach_money, size: size);
}
}
}

enum TransactionStatus {
failed,
Expand Down
4 changes: 1 addition & 3 deletions lib/features/home/transaction_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ class TransactionDetailsPage extends HookWidget {
const SizedBox(height: Grid.xs),
ExcludeSemantics(
child: Center(
child: txn.type == TransactionType.deposit
? const Icon(Icons.south_west, size: Grid.lg)
: const Icon(Icons.north_east, size: Grid.lg),
child: Transaction.getIcon(txn.type, size: Grid.lg),
),
),
const SizedBox(height: Grid.xxs),
Expand Down
25 changes: 2 additions & 23 deletions lib/features/tbdex/quote_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import 'dart:async';
import 'dart:math';

import 'package:didpay/config/config.dart';
import 'package:didpay/features/account/account_providers.dart';
import 'package:didpay/features/tbdex/tbdex_exceptions.dart';
import 'package:didpay/shared/http_status.dart';
import 'package:didpay/features/tbdex/tbdex_providers.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tbdex/tbdex.dart';

Expand Down Expand Up @@ -43,7 +40,7 @@ class QuoteAsyncNotifier extends AutoDisposeAsyncNotifier<Quote?> {

_timer = Timer.periodic(_currentInterval, (_) async {
try {
final exchange = await _fetchExchange(exchangeId);
final exchange = await ref.read(exchangeProvider(exchangeId).future);
if (_containsQuote(exchange)) {
state = AsyncValue.data(_getQuote(exchange));
stopPolling();
Expand Down Expand Up @@ -76,24 +73,6 @@ class QuoteAsyncNotifier extends AutoDisposeAsyncNotifier<Quote?> {
_currentInterval = _backoffIntervals.first;
}

Future<Exchange> _fetchExchange(String exchangeId) async {
final did = ref.read(didProvider);
final country = ref.read(countryProvider);
final pfi = Config.getPfi(country);

final response = await TbdexHttpClient.getExchange(
did,
pfi?.didUri ?? '',
exchangeId,
);

if (response.statusCode.category != HttpStatus.success) {
throw QuoteException('failed to retrieve quote', response.statusCode);
}

return response.data ?? [];
}

bool _containsQuote(Exchange exchange) =>
exchange.any((message) => message.metadata.kind == MessageKind.quote);

Expand Down
8 changes: 6 additions & 2 deletions lib/features/tbdex/tbdex_exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class OfferingException extends TbdexException {
OfferingException(super.message, super.errorCode);
}

class QuoteException extends TbdexException {
QuoteException(super.message, super.errorCode);
class ExchangeException extends TbdexException {
ExchangeException(super.message, super.errorCode);
}

class ExchangesException extends TbdexException {
ExchangesException(super.message, super.errorCode);
}
Loading

0 comments on commit 4463f8d

Please sign in to comment.