Skip to content

Commit 4d66ef5

Browse files
authored
feat: add package:fresh_http (#139)
1 parent 2205cbf commit 4d66ef5

File tree

26 files changed

+2417
-28
lines changed

26 files changed

+2417
-28
lines changed

.github/actions/dart_package/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ runs:
8484
dart pub global activate pana 0.21.45
8585
sudo apt-get install webp
8686
PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p")
87-
echo "score: $PANA_SCORE"
87+
echo "$PANA"
8888
IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1]
8989
if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi
9090
if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi

.github/actions/flutter_package/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ runs:
8080
dart pub global activate pana 0.21.45
8181
sudo apt-get install webp
8282
PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p")
83-
echo "score: $PANA_SCORE"
83+
echo "$PANA"
8484
IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1]
8585
if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi
8686
if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi

.github/workflows/main.yaml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ jobs:
4747
- ./.github/workflows/main.yaml
4848
- ./.github/actions/dart_package/action.yaml
4949
- packages/fresh_graphql/**
50+
- packages/fresh/**
51+
fresh_http:
52+
- ./.github/workflows/main.yaml
53+
- ./.github/actions/dart_package/action.yaml
54+
- packages/fresh_http/**
55+
- packages/fresh/**
5056
5157
- uses: dorny/paths-filter@v3
5258
name: Flutter Package Detection
@@ -57,10 +63,7 @@ jobs:
5763
- ./.github/workflows/main.yaml
5864
- ./.github/actions/dart_package/action.yaml
5965
- packages/fresh_dio/**
60-
fresh_dio/example:
61-
- ./.github/workflows/main.yaml
62-
- ./.github/actions/dart_package/action.yaml
63-
- packages/fresh_dio/**
66+
- packages/fresh/**
6467
6568
dart_package_checks:
6669
needs: changes
@@ -82,8 +85,8 @@ jobs:
8285
- name: 🎯 Build ${{ matrix.package }}
8386
uses: ./.github/actions/dart_package
8487
with:
85-
# TODO: remove when fresh v0.5.0 is published.
86-
collect_score: false
88+
# TODO: remove when fresh_http v0.1.0 is published.
89+
collect_score: ${{ matrix.package != 'fresh_http'}}
8790
min_coverage: 100
8891
working_directory: packages/${{ matrix.package }}
8992

@@ -107,8 +110,6 @@ jobs:
107110
- name: 🎯 Build ${{ matrix.package }}
108111
uses: ./.github/actions/flutter_package
109112
with:
110-
# TODO: remove when fresh v0.5.0 is published.
111-
collect_score: false
112113
min_coverage: 100
113114
working_directory: packages/${{ matrix.package }}
114115

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: publish/fresh_http
2+
3+
on:
4+
push:
5+
tags:
6+
- "fresh_http-v[0-9]+.[0-9]+.[0-9]+*"
7+
8+
jobs:
9+
publish:
10+
environment: pub.dev
11+
runs-on: ubuntu-latest
12+
permissions:
13+
id-token: write # Required for authentication using OIDC
14+
15+
steps:
16+
- name: 📚 Git Checkout
17+
uses: actions/checkout@v6
18+
19+
- name: 📦 Publish
20+
uses: ./.github/actions/pub_publish
21+
with:
22+
working_directory: packages/fresh_http

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Token-based authentication seems simple until you handle the edge cases: tokens
2323
| [fresh](https://github.com/felangel/fresh/tree/master/packages/fresh) | [![pub package](https://img.shields.io/pub/v/fresh.svg)](https://pub.dev/packages/fresh) |
2424
| [fresh_dio](https://github.com/felangel/fresh/tree/master/packages/fresh_dio) | [![pub package](https://img.shields.io/pub/v/fresh_dio.svg)](https://pub.dev/packages/fresh_dio) |
2525
| [fresh_graphql](https://github.com/felangel/fresh/tree/master/packages/fresh_graphql) | [![pub package](https://img.shields.io/pub/v/fresh_graphql.svg)](https://pub.dev/packages/fresh_graphql) |
26+
| [fresh_http](https://github.com/felangel/fresh/tree/master/packages/fresh_http) | [![pub package](https://img.shields.io/pub/v/fresh_http.svg)](https://pub.dev/packages/fresh_http) |
2627

2728
## Features
2829

@@ -86,3 +87,26 @@ final link = Link.from([freshLink, HttpLink('https://api.example.com/graphql')])
8687
```
8788

8889
See the [fresh_graphql README](https://github.com/felangel/fresh/tree/master/packages/fresh_graphql) for full documentation.
90+
91+
### fresh_http
92+
93+
```dart
94+
import 'dart:convert';
95+
96+
final client = Fresh.oAuth2(
97+
tokenStorage: InMemoryTokenStorage<OAuth2Token>(),
98+
refreshToken: (token, client) async {
99+
final response = await client.post(
100+
Uri.parse('https://api.example.com/auth/refresh'),
101+
body: jsonEncode({'refresh_token': token?.refreshToken}),
102+
);
103+
final body = jsonDecode(response.body) as Map<String, dynamic>;
104+
return OAuth2Token(
105+
accessToken: body['access_token'],
106+
refreshToken: body['refresh_token'],
107+
);
108+
},
109+
);
110+
```
111+
112+
See the [fresh_http README](https://github.com/felangel/fresh/tree/master/packages/fresh_http) for full documentation.

packages/fresh/lib/src/fresh.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ mixin FreshMixin<T> {
9393
/// returned synchronously (no microtask gap). Otherwise, waits for the
9494
/// initial storage read to complete.
9595
Future<T?> get token => Future.sync(() {
96-
if (_authenticationStatus != AuthenticationStatus.initial)
96+
if (_authenticationStatus != AuthenticationStatus.initial) {
9797
return _token;
98+
}
9899
return authenticationStatus
99100
.firstWhere((status) => status != AuthenticationStatus.initial)
100101
.then((_) => _token);

packages/fresh_dio/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,11 @@ Fresh.oAuth2(
180180
return false;
181181
},
182182
shouldRefreshBeforeRequest: (options, token) {
183-
// Refresh proactively if token expires within 30 seconds
183+
// Refresh proactively if token expires within 60 seconds
184+
// By default, a refresh is performed if the token expires within 30s.
184185
final expiresAt = token?.expiresAt;
185186
if (expiresAt == null) return false;
186-
return expiresAt.difference(DateTime.now()).inSeconds < 30;
187+
return expiresAt.difference(DateTime.now()).inSeconds < 60;
187188
},
188189
);
189190
```

packages/fresh_dio/lib/src/fresh.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ typedef IsTokenRequired = bool Function(RequestOptions options);
2929
///
3030
/// ```dart
3131
/// dio.interceptors.add(
32-
/// Fresh<AuthToken>(
32+
/// Fresh<OAuth2Token>(
3333
/// tokenStorage: InMemoryTokenStorage(),
3434
/// refreshToken: (token, client) async {...},
3535
/// ),
@@ -64,8 +64,8 @@ class Fresh<T> extends QueuedInterceptor with FreshMixin<T> {
6464
///
6565
/// ```dart
6666
/// dio.interceptors.add(
67-
/// Fresh.oAuth2(
68-
/// tokenStorage: InMemoryTokenStorage<AuthToken>(),
67+
/// Fresh.oAuth2<OAuth2Token>(
68+
/// tokenStorage: InMemoryTokenStorage(),
6969
/// refreshToken: (token, client) async {...},
7070
/// // Optional: control which requests require authentication
7171
/// isTokenRequired: (options) {
@@ -303,12 +303,9 @@ Example:
303303
) {
304304
if (token is Token) {
305305
final expiresAt = token.expiresAt;
306-
if (expiresAt != null) {
307-
final now = DateTime.now();
308-
return expiresAt.isBefore(now);
309-
}
306+
if (expiresAt == null) return false;
307+
return expiresAt.difference(DateTime.now()).inSeconds < 30;
310308
}
311-
312309
return false;
313310
}
314311
}

packages/fresh_dio/test/fresh_test.dart

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,112 @@ void main() {
10541054
});
10551055
});
10561056

1057+
group('_defaultShouldRefreshBeforeRequest', () {
1058+
test(
1059+
'does not refresh when token expires '
1060+
'more than 30 seconds from now', () async {
1061+
final token = OAuth2Token(
1062+
accessToken: 'accessToken',
1063+
issuedAt: DateTime.now(),
1064+
expiresIn: 60,
1065+
);
1066+
when(() => tokenStorage.read()).thenAnswer((_) async => token);
1067+
when(() => tokenStorage.write(any())).thenAnswer((_) async {});
1068+
1069+
var refreshCallCount = 0;
1070+
final fresh = Fresh.oAuth2<OAuth2Token>(
1071+
tokenStorage: tokenStorage,
1072+
refreshToken: (token, client) async {
1073+
refreshCallCount++;
1074+
return token ?? MockToken();
1075+
},
1076+
);
1077+
1078+
final options = RequestOptions();
1079+
await fresh.onRequest(options, requestHandler);
1080+
1081+
expect(refreshCallCount, 0);
1082+
});
1083+
1084+
test('refreshes when token expires within 30 seconds', () async {
1085+
final token = OAuth2Token(
1086+
accessToken: 'accessToken',
1087+
issuedAt: DateTime.now(),
1088+
expiresIn: 29,
1089+
);
1090+
when(() => tokenStorage.read()).thenAnswer((_) async => token);
1091+
when(() => tokenStorage.write(any())).thenAnswer((_) async {});
1092+
1093+
var refreshCallCount = 0;
1094+
final fresh = Fresh.oAuth2<OAuth2Token>(
1095+
tokenStorage: tokenStorage,
1096+
refreshToken: (_, __) async {
1097+
refreshCallCount++;
1098+
return const OAuth2Token(accessToken: 'newToken');
1099+
},
1100+
);
1101+
1102+
final options = RequestOptions();
1103+
await fresh.onRequest(options, requestHandler);
1104+
1105+
expect(refreshCallCount, 1);
1106+
verify(() => tokenStorage.write(any())).called(1);
1107+
});
1108+
1109+
test(
1110+
'refreshes when token expires '
1111+
'exactly at the 30 second boundary', () async {
1112+
final token = OAuth2Token(
1113+
accessToken: 'accessToken',
1114+
issuedAt: DateTime.now(),
1115+
expiresIn: 30,
1116+
);
1117+
when(() => tokenStorage.read()).thenAnswer((_) async => token);
1118+
when(() => tokenStorage.write(any())).thenAnswer((_) async {});
1119+
1120+
var refreshCallCount = 0;
1121+
final fresh = Fresh.oAuth2<OAuth2Token>(
1122+
tokenStorage: tokenStorage,
1123+
refreshToken: (token, client) async {
1124+
refreshCallCount++;
1125+
return token ?? MockToken();
1126+
},
1127+
);
1128+
1129+
final options = RequestOptions();
1130+
await fresh.onRequest(options, requestHandler);
1131+
1132+
expect(refreshCallCount, 1);
1133+
});
1134+
1135+
test('refreshes when token is already expired', () async {
1136+
final expiredToken = OAuth2Token(
1137+
accessToken: 'expiredToken',
1138+
issuedAt: DateTime.now().subtract(const Duration(hours: 2)),
1139+
expiresIn: 3600,
1140+
);
1141+
const newToken = OAuth2Token(accessToken: 'newToken');
1142+
1143+
when(() => tokenStorage.read()).thenAnswer((_) async => expiredToken);
1144+
when(() => tokenStorage.write(any())).thenAnswer((_) async {});
1145+
1146+
var refreshCallCount = 0;
1147+
final fresh = Fresh.oAuth2<OAuth2Token>(
1148+
tokenStorage: tokenStorage,
1149+
refreshToken: (_, __) async {
1150+
refreshCallCount++;
1151+
return newToken;
1152+
},
1153+
);
1154+
1155+
final options = RequestOptions();
1156+
await fresh.onRequest(options, requestHandler);
1157+
1158+
expect(refreshCallCount, 1);
1159+
verify(() => tokenStorage.write(any())).called(1);
1160+
});
1161+
});
1162+
10571163
group('close', () {
10581164
test('should close streams', () async {
10591165
final token = MockToken();

packages/fresh_graphql/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,11 @@ FreshLink.oAuth2(
143143
}) ?? false;
144144
},
145145
shouldRefreshBeforeRequest: (request, token) {
146-
// Refresh proactively if token expires within 30 seconds
146+
// Refresh proactively if token expires within 60 seconds
147+
// By default, a refresh is performed if the token expires within 30s.
147148
final expiresAt = token?.expiresAt;
148149
if (expiresAt == null) return false;
149-
return expiresAt.difference(DateTime.now()).inSeconds < 30;
150+
return expiresAt.difference(DateTime.now()).inSeconds < 60;
150151
},
151152
);
152153
```

0 commit comments

Comments
 (0)