Skip to content

Commit

Permalink
feat: implement a list transaction fragment
Browse files Browse the repository at this point in the history
refs #1
  • Loading branch information
fusion44 committed Dec 10, 2021
1 parent 1ad6312 commit 62cbb10
Show file tree
Hide file tree
Showing 12 changed files with 763 additions and 0 deletions.
16 changes: 16 additions & 0 deletions packages/common/lib/src/widgets/bottom_loader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';

class BottomLoader extends StatelessWidget {
const BottomLoader({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 1.5),
),
);
}
}
1 change: 1 addition & 0 deletions packages/common/lib/src/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'bottom_loader.dart';
export 'custom_dialog.dart';
export 'debug_view.dart';
export 'money_value_view.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
library list_transactions_fragment;

export 'src/bloc/list_tx_bloc.dart';
export 'src/models/model.dart';
export 'src/views/list_tx_view.dart';
export 'src/widgets/tx_list_item.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:async';
import 'dart:convert';

import 'package:authentication/authentication.dart';
import 'package:bloc/bloc.dart';
import 'package:common/common.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:subscription_repository/subscription_repository.dart';

import '../models/transaction.dart';

part 'list_tx_event.dart';
part 'list_tx_state.dart';

class ListTxBloc extends Bloc<ListTxEvent, ListTxState> {
final AuthRepo _authRepo;
final SubscriptionRepository _subRepo;
late final StreamSubscription<Map<String, dynamic>>? _sub;

bool _isLoading = false;

ListTxBloc(this._authRepo, this._subRepo) : super(const ListTxState()) {
on<LoadMoreTx>(_onLoadMoreTx);
_sub = _subRepo.filteredStream([
SseEventTypes.lnPaymentStatus,
SseEventTypes.lnInvoiceStatus,
])?.listen(
(event) {
// TODO: implement me. Update the status of the payment / invoice
},
);
}

void dispose() async {
await _sub?.cancel();
}

Future<void> _onLoadMoreTx(event, emit) async {
if (state.hasReachedMax || _isLoading) return;
try {
if (state.status == ListTxStatus.initial) {
final txs = await _fetchTransactions(event);
return emit(
state.copyWith(
status: ListTxStatus.success,
txs: txs,
hasReachedMax: false,
),
);
}
final transactions = await _fetchTransactions(event);
emit(
transactions.isEmpty
? state.copyWith(hasReachedMax: true)
: state.copyWith(
status: ListTxStatus.success,
txs: List.of(state.txs)..addAll(transactions),
hasReachedMax: false,
),
);
} catch (_) {
emit(state.copyWith(status: ListTxStatus.failure));
}
}

Future<List<Transaction>> _fetchTransactions(LoadMoreTx event) async {
_isLoading = true;
var i = state.txs.isEmpty ? 0 : state.txs.last.index + 1;
try {
final url =
'${_authRepo.baseUrl()}/latest/lightning/list-all-tx?index_offset=$i&max_tx=${event.maxTx}';
final response = await fetch(Uri.parse(url), _authRepo.token());
final js = jsonDecode(response.body);
final txs = <Transaction>[];
for (var tx in js) {
txs.add(Transaction.fromJson(tx));
}
return txs;
} catch (e) {
BlitzLog().e(e.toString());
} finally {
_isLoading = false;
}

return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
part of 'list_tx_bloc.dart';

@immutable
abstract class ListTxEvent {}

class LoadMoreTx extends ListTxEvent {
final int maxTx;

/// Loads more transactions up to [maxTx].
///
/// If [maxTx] is set to 0, the bloc attempt to load all
/// available transactions at once.
LoadMoreTx([this.maxTx = 25]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of 'list_tx_bloc.dart';

enum ListTxStatus { initial, success, failure }

class ListTxState extends Equatable {
const ListTxState({
this.status = ListTxStatus.initial,
this.txs = const <Transaction>[],
this.hasReachedMax = false,
});

final ListTxStatus status;
final List<Transaction> txs;
final bool hasReachedMax;

ListTxState copyWith({
ListTxStatus? status,
List<Transaction>? txs,
bool? hasReachedMax,
}) {
return ListTxState(
status: status ?? this.status,
txs: txs ?? this.txs,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
);
}

@override
String toString() {
return '''ListTxState { status: $status, hasReachedMax: $hasReachedMax, posts: ${txs.length} }''';
}

@override
List<Object> get props => [status, txs, hasReachedMax];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'transaction.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Transaction {
final int index;
final String id;
final String category;
final String type;
final int amount;
final int timeStamp;
final String comment;
final String status;
final int? blockHeight;
final int? numConfs;
final int? totalFees;

Transaction({
required this.index,
required this.id,
required this.category,
required this.type,
required this.amount,
required this.timeStamp,
required this.comment,
required this.status,
required this.blockHeight,
required this.numConfs,
required this.totalFees,
});

static Transaction fromJson(Map<String, dynamic> json) {
return Transaction(
index: json['index'] ?? -1,
id: json['id'] ?? '',
category: json['category'] ?? 'unknown',
type: json['type'] ?? 'unknown',
amount: json['amount'] ?? 0,
timeStamp: json['time_stamp'] ?? 0,
comment: json['comment'] ?? '',
status: json['status'] ?? 'unknown',
blockHeight: json['block_height'] ?? 0,
numConfs: json['num_confs'] ?? 0,
totalFees: json['total_fees'] ?? 0,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:common/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/list_tx_bloc.dart';
import '../widgets/tx_list_item.dart';

class FundsPage extends StatefulWidget {
final Function(bool) _setFABVisible;

const FundsPage(this._setFABVisible, {Key? key}) : super(key: key);

@override
_FundsPageState createState() => _FundsPageState();
}

class _FundsPageState extends State<FundsPage> {
late ScrollController _scrollController;
var _fabVisible = true;

@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_fabVisible == true) {
_fabVisible = false;
widget._setFABVisible(_fabVisible);
}
if (_isBottom) {
context.read<ListTxBloc>().add(LoadMoreTx());
}
} else {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.forward) {
if (_fabVisible == false) {
_fabVisible = true;
widget._setFABVisible(_fabVisible);
}
}
}
});
}

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

_scrollController.dispose();
}

@override
Widget build(BuildContext context) {
return BlocBuilder<ListTxBloc, ListTxState>(
builder: (context, txState) {
switch (txState.status) {
case ListTxStatus.failure:
return const Center(child: Text('Failed to fetch transactions'));
case ListTxStatus.success:
if (txState.txs.isEmpty) {
return const Center(child: Text('no transactions'));
}

return ListView.builder(
itemBuilder: (BuildContext context, int i) {
return i >= txState.txs.length
? const BottomLoader()
: TxListItem(tx: txState.txs[i]);
},
itemCount: txState.hasReachedMax
? txState.txs.length
: txState.txs.length + 1,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
);
}

bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import '../models/transaction.dart';

class TxListItem extends StatelessWidget {
const TxListItem({Key? key, required this.tx}) : super(key: key);

final Transaction tx;

@override
Widget build(BuildContext context) {
return ListTile(
leading: Text('${tx.index}'),
title: Text(tx.category),
);
}
}
Loading

0 comments on commit 62cbb10

Please sign in to comment.