Skip to content

Commit 774a299

Browse files
feat: add regular poll for session token [CLERK_SDK #42] (#43) (#48)
* feat: add regular poll for session token [CLERK_SDK #42] * feat: add poll mode configuration to clerk_flutter [CLERK_SDK #42] * fix: pr changes --------- Co-authored-by: Nic Ford <[email protected]>
1 parent bfe55f3 commit 774a299

File tree

17 files changed

+255
-123
lines changed

17 files changed

+255
-123
lines changed

packages/clerk_auth/.pubignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ build/
1111

1212
# Dont need to package this
1313
build.yaml
14+
15+
# Test environment
16+
.env.test

packages/clerk_auth/lib/src/clerk_api/api.dart

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,53 @@ import 'package:http/http.dart' as http;
88

99
export 'package:clerk_auth/src/models/models.dart';
1010

11+
/// [SessionTokenPollMode] manages how to refresh the [sessionToken]
12+
///
13+
enum SessionTokenPollMode {
14+
/// Refresh whenever token expires (more http access and power use)
15+
regular,
16+
17+
/// Refresh if expired when accessed (with possible increased latency at that time)
18+
onDemand;
19+
}
20+
1121
/// [Api] manages communication with the Clerk frontend API
1222
///
1323
class Api with Logging {
14-
Api._(this._tokenCache, this._domain, this._client);
24+
Api._(this._tokenCache, this._domain, this._client, this._pollMode);
1525

1626
/// Create an [Api] object for a given public key, or return the existing one
1727
/// if such already exists for that key. Requires a [publicKey] and [publishableKey]
18-
/// found in the Clerk dashboard for you account. Can also take an optional http [client]
19-
/// and [persistor]
28+
/// found in the Clerk dashboard for you account. Additional arguments:
29+
///
30+
/// [persistor]: an optional instance of a [Persistor] which will keep track of
31+
/// tokens and expiry between app activations
32+
///
33+
/// [client]: an optional instance of [HttpClient] to manage low-level communications
34+
/// with the back end. Injected for e.g. test mocking
35+
///
36+
/// [pollMode]: session token poll mode, default on-demand,
37+
/// manages how to refresh the [sessionToken].
38+
///
2039
factory Api({
2140
required String publishableKey,
2241
required String publicKey,
42+
Persistor persistor = Persistor.none,
43+
SessionTokenPollMode pollMode = SessionTokenPollMode.onDemand,
2344
HttpClient? client,
24-
Persistor? persistor,
2545
}) =>
2646
_caches[publicKey] ??= Api._(
2747
TokenCache(publicKey, persistor),
2848
_deriveDomainFrom(publishableKey),
29-
client ?? DefaultHttpClient(),
49+
client ?? const DefaultHttpClient(),
50+
pollMode,
3051
);
3152

3253
final TokenCache _tokenCache;
3354
final String _domain;
3455
final HttpClient _client;
56+
final SessionTokenPollMode _pollMode;
57+
Timer? _pollTimer;
3558

3659
static final _caches = <String, Api>{};
3760

@@ -44,6 +67,19 @@ class Api with Logging {
4467
static const _kClientKey = 'client';
4568
static const _kResponseKey = 'response';
4669

70+
/// Initialise the API
71+
Future<void> initialize() async {
72+
await _tokenCache.initialize();
73+
if (_pollMode == SessionTokenPollMode.regular) {
74+
await _pollForSessionToken();
75+
}
76+
}
77+
78+
/// Dispose of the API
79+
void terminate() {
80+
_pollTimer?.cancel();
81+
}
82+
4783
// environment & client
4884

4985
/// the domain of the Clerk front-end API server
@@ -453,6 +489,16 @@ class Api with Logging {
453489
return _tokenCache.sessionToken;
454490
}
455491

492+
Future<void> _pollForSessionToken() async {
493+
_pollTimer?.cancel();
494+
495+
await sessionToken(); // make sure updated
496+
497+
final diff = _tokenCache.sessionTokenExpiry.difference(DateTime.now());
498+
final delay = diff.isNegative ? const Duration(seconds: 55) : diff;
499+
_pollTimer = Timer(delay, _pollForSessionToken);
500+
}
501+
456502
// Internal
457503

458504
Future<ApiResponse> _fetchApiResponse(

packages/clerk_auth/lib/src/clerk_api/token_cache.dart

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import 'package:logging/logging.dart';
1313
class TokenCache {
1414
/// Create a [TokenCache] instance
1515
///
16-
TokenCache(this._publicKey, this._persistor) {
17-
_init();
18-
}
16+
TokenCache(this._publicKey, this._persistor);
1917

2018
final String _publicKey;
21-
final Persistor? _persistor;
19+
final Persistor _persistor;
20+
21+
DateTime _sessionTokenExpiry = DateTime.fromMillisecondsSinceEpoch(0);
22+
23+
/// the date at which, if in the future, the current [sessionToken]
24+
/// is due to expire
25+
DateTime get sessionTokenExpiry => _sessionTokenExpiry;
2226

2327
static const _tokenExpiryBuffer = Duration(seconds: 10);
2428

@@ -28,14 +32,13 @@ class TokenCache {
2832
String _sessionId = '';
2933
String _clientToken = '';
3034
String _sessionToken = '';
31-
DateTime _sessionTokenExpiry = DateTime.fromMillisecondsSinceEpoch(0);
3235

3336
/// Whether or not the [sessionToken] can be refreshed
3437
bool get canRefreshSessionToken =>
3538
clientToken.isNotEmpty && sessionId.isNotEmpty;
3639

3740
bool get _sessionTokenHasExpired =>
38-
DateTime.now().isAfter(_sessionTokenExpiry);
41+
DateTime.now().isAfter(sessionTokenExpiry);
3942

4043
String get _sessionIdKey => '_clerkSessionId_${_publicKey.hashCode}';
4144

@@ -53,19 +56,23 @@ class TokenCache {
5356
_clientTokenKey,
5457
];
5558

56-
Future<void> _init() async {
59+
/// Initialise the cache
60+
Future<void> initialize() async {
5761
_rsaKey = RSAPublicKey(_publicKey);
58-
if (_persistor case Persistor persistor) {
59-
final [sessionId, sessionToken, ms, clientToken] = await Future.wait(
60-
_persistorKeys.map(persistor.read),
61-
);
62-
63-
_sessionId = sessionId ?? '';
64-
_sessionToken = sessionToken ?? '';
65-
_clientToken = clientToken ?? '';
66-
_sessionTokenExpiry =
67-
DateTime.fromMillisecondsSinceEpoch(int.tryParse(ms ?? '') ?? 0);
68-
}
62+
63+
// Read all stored variables first before assignment
64+
final sessionId = await _persistor.read(_sessionIdKey) ?? '';
65+
final sessionToken = await _persistor.read(_sessionTokenKey) ?? '';
66+
final clientToken = await _persistor.read(_clientTokenKey) ?? '';
67+
final milliseconds = await _persistor.read(_sessionTokenExpiryKey) ?? '';
68+
final sessionTokenExpiry = DateTime.fromMillisecondsSinceEpoch(
69+
int.tryParse(milliseconds) ?? 0,
70+
);
71+
72+
_sessionId = sessionId;
73+
_sessionToken = sessionToken;
74+
_clientToken = clientToken;
75+
_sessionTokenExpiry = sessionTokenExpiry;
6976
}
7077

7178
/// Reset the [TokenCache]
@@ -76,7 +83,7 @@ class TokenCache {
7683
_sessionToken = '';
7784
_sessionTokenExpiry = DateTime.fromMillisecondsSinceEpoch(0);
7885
for (final key in _persistorKeys) {
79-
_persistor?.delete(key);
86+
_persistor.delete(key);
8087
}
8188
}
8289

@@ -86,7 +93,7 @@ class TokenCache {
8693
/// Set the [sessionId]
8794
set sessionId(String id) {
8895
_sessionId = id;
89-
_persistor?.write(_sessionIdKey, id);
96+
_persistor.write(_sessionIdKey, id);
9097
}
9198

9299
/// Get the [clientToken]
@@ -99,7 +106,7 @@ class TokenCache {
99106
try {
100107
JWT.verify(token, _rsaKey);
101108
_clientToken = token;
102-
_persistor?.write(_clientTokenKey, token);
109+
_persistor.write(_clientTokenKey, token);
103110
} catch (error, stackTrace) {
104111
_logger.severe('ERROR SETTING CLIENT TOKEN: $error', error, stackTrace);
105112
}
@@ -117,15 +124,16 @@ class TokenCache {
117124

118125
try {
119126
final jwt = JWT.verify(token, _rsaKey);
120-
final exp = jwt.payload['exp'];
121-
if (exp is int) {
122-
final expiry = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
127+
final expirySeconds = jwt.payload['exp'];
128+
if (expirySeconds is int) {
129+
final expiry = DateTime.fromMillisecondsSinceEpoch(
130+
expirySeconds * Duration.millisecondsPerSecond);
123131
_sessionTokenExpiry = expiry.subtract(_tokenExpiryBuffer);
124132
_sessionToken = token;
125-
_persistor?.write(_sessionTokenKey, token);
126-
_persistor?.write(
133+
_persistor.write(_sessionTokenKey, token);
134+
_persistor.write(
127135
_sessionTokenExpiryKey,
128-
_sessionTokenExpiry.millisecondsSinceEpoch.toString(),
136+
sessionTokenExpiry.millisecondsSinceEpoch.toString(),
129137
);
130138
}
131139
} catch (error, _) {

packages/clerk_auth/lib/src/clerk_auth/auth.dart

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,32 @@ export 'persistor.dart';
88

99
/// [Auth] provides more abstracted access to the Clerk API
1010
///
11+
/// Requires a [publicKey] and [publishableKey] found in the Clerk dashboard
12+
/// for you account. Additional arguments:
13+
///
14+
/// [persistor]: an optional instance of a [Persistor] which will keep track of
15+
/// tokens and expiry between app activations
16+
///
17+
/// [client]: an optional instance of [HttpClient] to manage low-level communications
18+
/// with the back end. Injected for e.g. test mocking
19+
///
20+
/// [pollMode]: session token poll mode, default on-demand,
21+
/// manages how to refresh the [sessionToken].
22+
///
1123
class Auth {
1224
/// Create an [Auth] object using appropriate Clerk credentials
1325
Auth({
14-
required String publishableKey,
1526
required String publicKey,
16-
Persistor? persistor,
27+
required String publishableKey,
28+
required Persistor persistor,
1729
HttpClient? client,
30+
SessionTokenPollMode pollMode = SessionTokenPollMode.onDemand,
1831
}) : _api = Api(
1932
publicKey: publicKey,
2033
publishableKey: publishableKey,
2134
persistor: persistor,
2235
client: client,
36+
pollMode: pollMode,
2337
);
2438

2539
final Api _api;
@@ -64,12 +78,26 @@ class Auth {
6478

6579
/// Initialisation of the [Auth] object
6680
///
67-
/// [init] must be called before any further use of the [Auth]
81+
/// [initialize] must be called before any further use of the [Auth]
6882
/// object is made
6983
///
70-
Future<void> init() async {
71-
client = await _api.createClient();
72-
env = await _api.environment();
84+
Future<void> initialize() async {
85+
await _api.initialize();
86+
final [client, env] = await Future.wait([
87+
_api.createClient(),
88+
_api.environment(),
89+
]);
90+
this.client = client as Client;
91+
this.env = env as Environment;
92+
}
93+
94+
/// Disposal of the [Auth] object
95+
///
96+
/// Named [terminate] so as not to clash with [ChangeNotifier]'s [dispose]
97+
/// method, if that is mixed in e.g. in clerk_flutter
98+
///
99+
void terminate() {
100+
_api.terminate();
73101
}
74102

75103
/// Refresh the current [Client]
@@ -147,7 +175,7 @@ class Auth {
147175
switch (client.signIn) {
148176
case SignIn signIn when strategy.isOauth == true && token is String:
149177
await _api
150-
.sendOauthToken(signIn, strategy: strategy!, token: token)
178+
.sendOauthToken(signIn, strategy: strategy, token: token)
151179
.then(_housekeeping);
152180

153181
case SignIn signIn
@@ -191,7 +219,7 @@ class Auth {
191219
final stage = Stage.forStatus(signIn.status);
192220
if (signIn.verificationFor(stage) is! Verification) {
193221
await _api
194-
.prepareSignIn(signIn, stage: stage, strategy: strategy!)
222+
.prepareSignIn(signIn, stage: stage, strategy: strategy)
195223
.then(_housekeeping);
196224
}
197225
if (client.signIn case SignIn signIn

packages/clerk_auth/lib/src/clerk_auth/http_client.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ enum HttpMethod {
3232
/// Clerk back-end over http
3333
///
3434
abstract class HttpClient {
35+
/// Constructor
36+
const HttpClient();
37+
3538
/// [send] date to the back end, and receive a [Response]
3639
///
3740
Future<Response> send(
@@ -45,6 +48,9 @@ abstract class HttpClient {
4548
/// Default implementation of [HttpClient]
4649
///
4750
class DefaultHttpClient implements HttpClient {
51+
/// Constructor
52+
const DefaultHttpClient();
53+
4854
@override
4955
Future<Response> send(
5056
HttpMethod method,

packages/clerk_auth/lib/src/clerk_auth/persistor.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
/// required to allow seamless auth across app runs
33
///
44
abstract class Persistor {
5+
/// Persistor used when no persistence is required
6+
static const none = _NonePersistor();
7+
58
/// Persist a [value] against a [key]
69
///
710
Future<void> write(String key, String value);
@@ -14,3 +17,16 @@ abstract class Persistor {
1417
///
1518
Future<void> delete(String key);
1619
}
20+
21+
final class _NonePersistor implements Persistor {
22+
const _NonePersistor();
23+
24+
@override
25+
Future<void> write(String key, String value) async {}
26+
27+
@override
28+
Future<String?> read(String key) => Future.value(null);
29+
30+
@override
31+
Future<void> delete(String key) async {}
32+
}

packages/clerk_auth/lib/src/models/api_response.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'models.dart';
44

55
/// [ApiResponse] holds parsed Clerk data from a back-end http response
66
class ApiResponse {
7+
/// Constructs an instance of [ApiResponse]
78
const ApiResponse({
89
required this.status,
910
this.errors,

packages/clerk_auth/lib/src/models/environment/auth_config.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class AuthConfig {
6262
@JsonKey(fromJson: toStrategyList)
6363
final List<Strategy> secondFactors;
6464

65+
/// email address verification strategy
6566
@JsonKey(fromJson: toStrategyList)
6667
final List<Strategy> emailAddressVerificationStrategies;
6768

packages/clerk_auth/lib/src/models/environment/display_config.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import 'package:json_annotation/json_annotation.dart';
22

33
part 'display_config.g.dart';
44

5+
/// Display Configuration
6+
///
57
@JsonSerializable()
68
class DisplayConfig {
9+
/// Constructs an instance of [DisplayConfig]
710
const DisplayConfig({
811
this.id = '',
912
this.applicationName = '',

packages/clerk_auth/lib/src/models/environment/user_settings.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ Map<UserAttribute, UserAttributeData> _toAttributeMap(dynamic data) {
7777
for (final entry in data.entries) {
7878
final key = UserAttribute.values
7979
.firstWhereOrNull((a) => a.toString() == entry.key);
80-
if (key case UserAttribute key)
80+
if (key case UserAttribute key) {
8181
result[key] = UserAttributeData.fromJson(entry.value);
82+
}
8283
}
8384
}
8485
return result;

0 commit comments

Comments
 (0)