Skip to content

Commit

Permalink
v3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Sominemo committed Nov 10, 2024
1 parent bcff44f commit 67ff139
Show file tree
Hide file tree
Showing 9 changed files with 1,479 additions and 235 deletions.
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@
## 3.0.0

### Breaking Changes

- All mentions of `Account` in statement-related classes were replaced
with `StatementSource`
- `BankCard.type` and `Account.type` are now a `CardTypeClass`
instance instead of a `CardType` enum
- In `Statement.list`, `reverse` parameter was renamed to `isReverseChronological`
and now defaults to `true`
- In `Statement.list`, `ignoreErrors` parameter was removed
- The statement stream may now include service messages, notifying about possible
discrepancies in the data. Right now such messages may be yielded when response length
equals `maxPossibleItems` and the requested time range can't be smaller. This means
some data might get truncated in the requested time range. You can check for
service messages using `StatementItem.isServiceMessage`. You can also
set `ignoreServiceMessages` to `true`.
- `CashbackType.toString` now returns the same kind of string as API returns
instead of enum name
- `LazyAccount` was replaced with `LazyStatementSource`, the class was completely
removed. The change also impacts `LazyStatementItem` class. You can cast the
result of the `LazyStatementSource.resolve` method to `Account` or `Jar` to
get specific fields.

### What's New

- `Account` and `Jar` are now subclasses of `StatementSource`,
this allows to fetch statements from jars too now using `StatementSource.statement`
- Added support for `madeInUkraine` card type
- Added access to raw card type string in `BankCard.type.raw`
- Exposed `lastRequestsTimes` and `globalLastRequestTime` in API class
to allow for state restoration and notifying the user about rate limits
- Added fields and data types related to service messages in statement items
- Added `AbortController` class to API to allow for cancelling requests.
If the request is still in the cart, it will not be sent to the server
when its turn comes. `APIError` with code 3 will be thrown as a response.
- Added support for `AbortController` in `APIRequest`
- Added `abortController` argument to `Statement.list`
- Added methods `BankCard.isMastercard` and `BankCard.isVisa`
- Made most of constructors in mono library public, including `fromJSON`,
to support persistence
- Added `toJSON()` methods to many classes in mono library for persistence
support
- `APIError` now has a new type - `isCancelled`, triggered when the request
was cancelled using `AbortController`
- Added `Cashback.fromType` factory
- Added `StatementItemServiceMessageCode` enum to distinguish between different
service message types

### What's Changed

- Rewritten `Statement.list` to handle cases when more than maxPossibleItems
are returned by the API
- `Statement.maxRequestRange` is now set to 31 days and 1 hour instead of 31 days
- Made `Statement.maxRequestRange` and `Statement.maxPossibleItems`
modifiable
- `API.globalTimeout` is not final anymore

## 2.1.0

- Bump http to 1.1.x
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ void main() async {
var res = await client.clientInfo();
var account = res.accounts[0];
var statement = account.statement(
DateTime.now().subtract(Duration(days: 31)), DateTime.now());
DateTime.now().subtract(Duration(days: 31)),
DateTime.now(),
);
await for (var item in statement.list(reverse: true)) {
await for (var item in statement.list(isReverseChronological: true)) {
print('$item');
}
}
Expand Down
29 changes: 21 additions & 8 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,35 @@ import 'package:monobank_api/mcc/extensions/mcc_emoji.dart';
// Connecting names dataset for Currency
import 'package:monobank_api/currency/extensions/currency_names.dart';

void statement() async {
// Get client statement for last 3 months from the first account
void main() async {
// Create client
final client = MonoAPI('token');
final mono = MonoAPI('token');

// Request client
final res = await client.clientInfo();
final client = await mono.clientInfo();

// Get first account
final account = res.accounts[0];
// List accounts and cards
for (final account in client.accounts) {
print('$account');
for (final card in account.cards) {
print(' $card');
}
}

// List jars
for (final jar in client.jars) {
print('$jar');
}

// Get statement list for last 3 months
final statement = account.statement(
DateTime.now().subtract(Duration(days: 31 * 3)), DateTime.now());
final statement = client.accounts[0].statement(
DateTime.now().subtract(Duration(days: 31 * 3)),
DateTime.now(),
);

// For each statement item
await for (final item in statement.list(reverse: true)) {
await for (final item in statement.list(isReverseChronological: true)) {
// Output string representation
print('${item.mcc.emoji} $item (${item.operationAmount.currency.name})');
}
Expand Down
129 changes: 106 additions & 23 deletions lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class APIError {
/// mutually-incompatible flags or passing invalid parameters
bool get isIllegalRequestError => type == APIErrorType.local && data == 2;

/// Checks if the request was cancelled by the user
///
/// This means the request was cancelled by the user using [AbortController.cancel]
bool get isCancelled => type == APIErrorType.local && data == 3;

/// Generates token error
///
/// Specifies behavior of the library when [APIRequest.useAuth] == true is being used on
Expand Down Expand Up @@ -146,9 +151,11 @@ class APIRequest {
/// ```
Map<String, String> headers = const {},
this.httpMethod = APIHttpMethod.GET,
AbortController? abortController,
}) : _completer = Completer(),
data = Map.from(data),
headers = Map.from(headers),
abortController = abortController ?? AbortController(),
_originalData = data,
_originalHeaders = headers;

Expand Down Expand Up @@ -196,6 +203,11 @@ class APIRequest {
/// See [APIHttpMethod]
final APIHttpMethod httpMethod;

/// Abort controller
///
/// See [AbortController]
final AbortController abortController;

final Completer<APIResponse> _completer;
bool _isProcessingNeeded = true;

Expand All @@ -215,14 +227,16 @@ class APIRequest {
/// pending result).
///
/// If you use the same [APIRequest] instance with different
/// [API] instances behavior is unspecified
factory APIRequest.clone(APIRequest request) => APIRequest(request.method,
methodId: request.methodId,
data: request._originalData,
headers: request._originalHeaders,
httpMethod: request.httpMethod,
settings: request.settings,
useAuth: request.useAuth);
/// [API] instances, behavior is undefined
factory APIRequest.clone(APIRequest request) => APIRequest(
request.method,
methodId: request.methodId,
data: request._originalData,
headers: request._originalHeaders,
httpMethod: request.httpMethod,
settings: request.settings,
useAuth: request.useAuth,
);
}

/// Response given by calling on [APIRequest]
Expand Down Expand Up @@ -295,13 +309,35 @@ class API {
///
/// At least this amount of time should pass before next request
/// will be sent
final Duration globalTimeout;
Duration globalTimeout;

final Set<APIRequest> _cart = {};
final Map<String, DateTime> _lastRequests = {};

/// A map of last time a request was sent by methodId
///
/// A map, where keys are [APIRequest.methodId] and values are [DateTime]
/// of when the request was received by the server.
///
/// This field is being used to throttle requests of the same class to
/// follow the rules specified in [API.requestTimeouts].
///
/// It is exposed publicly to allow you to save and restore the state
/// of the API instance.
///
/// See also: [globalLastRequestTime]
final Map<String, DateTime> lastRequestsTimes = {};
final Map<String, bool> _methodBusy = {};

DateTime _lastRequest = DateTime.fromMillisecondsSinceEpoch(0);
/// Last time any kind of request was sent
///
/// This field is being used to throttle requests to follow the rules
/// specified in [API.globalTimeout].
///
/// It is exposed publicly to allow you to save and restore the state
/// of the API instance.
///
/// See also: [lastRequestsTimes]
DateTime globalLastRequestTime = DateTime.fromMillisecondsSinceEpoch(0);
bool _cartBusy = false;

/// Minimal timeout between requests of the same class
Expand Down Expand Up @@ -340,8 +376,8 @@ class API {
///
/// Returns `DateTime.fromMillisecondsSinceEpoch(0)` if never was sent
DateTime lastRequest({String? methodId}) => methodId == null
? _lastRequest
: (_lastRequests[methodId] ?? DateTime.fromMillisecondsSinceEpoch(0));
? globalLastRequestTime
: (lastRequestsTimes[methodId] ?? DateTime.fromMillisecondsSinceEpoch(0));

/// Returns minimum required time until request \[of class\] can be sent
///
Expand All @@ -351,7 +387,7 @@ class API {
/// **minimum** required time, not the actual one
Duration willFreeIn({String? methodId}) {
// Time since last request
final timePassed = DateTime.now().difference(_lastRequest);
final timePassed = DateTime.now().difference(globalLastRequestTime);

// Return delay caused by global timer
// if not enough time has passed yet
Expand Down Expand Up @@ -507,12 +543,17 @@ class API {

try {
request._isProcessingNeeded = false;

if (request.abortController.isCancelled) {
throw APIError(3);
}

final inLine = (request.settings & APIFlags.skip == 0) &&
(request.settings & APIFlags.skipGlobal == 0);
_lastRequest = DateTime.now();
globalLastRequestTime = DateTime.now();

if (methodId != null) {
_lastRequests[methodId] = DateTime.now();
lastRequestsTimes[methodId] = DateTime.now();
if (inLine) {
_methodBusy[methodId] = true;
}
Expand All @@ -527,7 +568,7 @@ class API {

if (request.httpMethod == APIHttpMethod.POST) {
response = await http.post(requestUrl,
body: jsonEncode(request.data), headers: request.headers);
body: json.encode(request.data), headers: request.headers);
} else if (request.httpMethod == APIHttpMethod.GET) {
response = await http.get(requestUrl, headers: request.headers);
} else {
Expand All @@ -543,16 +584,16 @@ class API {
json.decode(response.body), response.statusCode, response.headers);

final stamp = DateTime.now();
_lastRequest = stamp;
if (methodId != null) _lastRequests[methodId] = stamp;
globalLastRequestTime = stamp;
if (methodId != null) lastRequestsTimes[methodId] = stamp;

if (inLine) {
if (methodId != null) _methodBusy[methodId] = false;
}
request._completer.complete(resolver);
} on APIError catch (e) {
_lastRequest = beforeSentTime;
if (methodId != null) _lastRequests[methodId] = beforeSentMethodTime;
globalLastRequestTime = beforeSentTime;
if (methodId != null) lastRequestsTimes[methodId] = beforeSentMethodTime;

if ((e.isFloodError && request.settings & APIFlags.resendOnFlood > 0) ||
request.settings & APIFlags.resend > 0) {
Expand All @@ -561,8 +602,8 @@ class API {
request._completer.completeError(e);
}
} catch (e) {
_lastRequest = beforeSentTime;
if (methodId != null) _lastRequests[methodId] = beforeSentMethodTime;
globalLastRequestTime = beforeSentTime;
if (methodId != null) lastRequestsTimes[methodId] = beforeSentMethodTime;

request._completer.completeError(e);
rethrow;
Expand All @@ -585,3 +626,45 @@ class API {
request.headers.putIfAbsent('Accept', () => 'application/json');
}
}

/// Abort controller
///
/// Allows to cancel the request.
///
/// Call [AbortController.cancel] to cancel the request.
class AbortController {
/// Create a new controller
AbortController() : _isCancelled = false;

/// Create a new controller and cancel it immediately
AbortController.cancelled() : _isCancelled = true;

bool _isCancelled;

/// Cancel the request
///
/// This will call all listeners that were added to the controller
void cancel() {
_isCancelled = true;

for (final listener in _listeners) {
listener?.call();
}
_listeners.clear();
}

/// Check if the request is cancelled
bool get isCancelled => _isCancelled;

/// Add a listener to the controller
///
/// Returns a function that removes the listener. The listener is
/// called when the request is cancelled
void Function() addListener(dynamic Function() callback) {
_listeners.add(callback);
final id = _listeners.length - 1;
return () => _listeners[id] = null;
}

final List<dynamic Function()?> _listeners = [];
}
2 changes: 1 addition & 1 deletion lib/src/mcc/mcc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class MCC {
///
/// Only MCC objects are allowed to be compared
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
if (other is! MCC) return false;
return code == other.code;
}
Expand Down
6 changes: 3 additions & 3 deletions lib/src/money.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class Currency {
/// Use [Currency.dummy] for money manipulations which don't
/// involve currency
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Currency) return false;
if (this is UnknownCurrency || other is UnknownCurrency) return false;
Expand Down Expand Up @@ -132,7 +132,7 @@ class UnknownCurrency extends Currency {
int get hashCode => 0;

@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
return identical(this, other);
}
}
Expand Down Expand Up @@ -239,7 +239,7 @@ class Money implements Comparable<Money> {
///
/// Compares amount of the instances
@override
bool operator ==(dynamic other) {
bool operator ==(Object other) {
if (other is! Money) throw Exception('Money can be compared only to Money');
if (currency != other.currency) return false;

Expand Down
Loading

0 comments on commit 67ff139

Please sign in to comment.