Skip to content

Commit ff4d57a

Browse files
authored
fix: move session token polling from api to auth for better error reporting [#244] (#252)
* feat: bring sign up ux in line with other sdks [#246] # Conflicts: # packages/clerk_flutter/lib/src/widgets/authentication/clerk_sign_up_panel.dart * fix: refactor the sign up panel build method [#246] * fix: refactors [#246] * fix: fix rebase discrepancies in sign up [#246] * fix: move session token polling from api to auth for better error reporting [#244] # Conflicts: # packages/clerk_auth/lib/src/clerk_api/api.dart * fix: resolve discrepancies from rebase [#244] * fix: simplify first build logic [#244]
1 parent 257bc83 commit ff4d57a

File tree

10 files changed

+155
-98
lines changed

10 files changed

+155
-98
lines changed

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

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ class Api with Logging {
2323
///
2424
Api({
2525
required this.config,
26-
this.sessionTokenSink,
2726
}) : _tokenCache = TokenCache(
2827
persistor: config.persistor,
2928
publishableKey: config.publishableKey,
@@ -34,14 +33,10 @@ class Api with Logging {
3433
/// The config used to initialize this api instance.
3534
final AuthConfig config;
3635

37-
/// The [Sink] for session tokens
38-
final Sink<SessionToken>? sessionTokenSink;
39-
4036
final TokenCache _tokenCache;
4137
final String _domain;
4238

4339
bool _testMode;
44-
Timer? _pollTimer;
4540
bool _multiSessionMode = true;
4641

4742
static const _kClerkAPIVersion = 'clerk-api-version';
@@ -60,19 +55,14 @@ class Api with Logging {
6055
static const _kXMobile = 'x-mobile';
6156
static const _scheme = 'https';
6257

63-
static const _defaultPollDelay = Duration(seconds: 53);
64-
6558
/// Initialise the API
6659
Future<void> initialize() async {
6760
await _tokenCache.initialize();
68-
if (config.sessionTokenPolling) {
69-
await _pollForSessionToken();
70-
}
7161
}
7262

7363
/// Dispose of the API
7464
void terminate() {
75-
_pollTimer?.cancel();
65+
_tokenCache.terminate();
7666
}
7767

7868
/// Confirm connectivity to the back end
@@ -813,18 +803,15 @@ class Api with Logging {
813803

814804
// Session
815805

816-
/// Return the [SessionToken] for the current active [Session], refreshing it
817-
/// if required
806+
/// Return the [SessionToken] for the current active [Session], if
807+
/// available
818808
///
819-
Future<SessionToken?> sessionToken([
820-
Organization? org,
821-
String? templateName,
822-
]) async {
823-
return _tokenCache.sessionTokenFor(org, templateName) ??
824-
await _updateSessionToken(org, templateName);
825-
}
809+
SessionToken? sessionToken([Organization? org, String? templateName]) =>
810+
_tokenCache.sessionTokenFor(org, templateName);
826811

827-
Future<SessionToken?> _updateSessionToken([
812+
/// Refresh and return the [SessionToken] for the current active [Session]
813+
///
814+
Future<SessionToken?> updateSessionToken([
828815
Organization? org,
829816
String? templateName,
830817
]) async {
@@ -843,26 +830,22 @@ class Api with Logging {
843830
_kOrganizationId: org.id,
844831
},
845832
);
833+
final body = json.decode(resp.body) as _JsonObject;
846834
if (resp.statusCode == HttpStatus.ok) {
847-
final body = json.decode(resp.body) as _JsonObject;
848835
final token = body[_kJwtKey] as String;
849-
final sessionToken =
850-
_tokenCache.makeAndCacheSessionToken(token, templateName);
851-
sessionTokenSink?.add(sessionToken);
852-
return sessionToken;
836+
return _tokenCache.makeAndCacheSessionToken(token, templateName);
837+
} else if (_extractErrorCollection(body) case ApiErrorCollection errors) {
838+
throw AuthError.from(errors);
839+
} else {
840+
throw const AuthError(
841+
message: 'No session token retrieved',
842+
code: AuthErrorCode.noSessionTokenRetrieved,
843+
);
853844
}
854845
}
855846
return null;
856847
}
857848

858-
Future<void> _pollForSessionToken() async {
859-
_pollTimer?.cancel();
860-
861-
final sessionToken = await _updateSessionToken();
862-
final delay = sessionToken?.ttl ?? _defaultPollDelay;
863-
_pollTimer = Timer(delay, _pollForSessionToken);
864-
}
865-
866849
// Internal
867850

868851
Future<ApiResponse> _uploadFile(HttpMethod method, Uri uri, File file) async {
@@ -922,22 +905,20 @@ class Api with Logging {
922905

923906
ApiResponse _processResponse(http.Response resp) {
924907
final body = json.decode(resp.body) as _JsonObject;
925-
final errors = _extractErrors(body[_kErrorsKey]);
908+
final errorCollection = _extractErrorCollection(body);
926909
final (clientData, responseData) = _extractClientAndResponse(body);
927910
if (clientData is _JsonObject) {
928911
final client = Client.fromJson(clientData);
929912
_tokenCache.updateFrom(resp, client);
930913
return ApiResponse(
931914
client: client,
932915
status: resp.statusCode,
933-
errors: errors,
916+
errorCollection: errorCollection,
934917
response: responseData,
935918
);
936919
} else {
937920
return ApiResponse(
938-
status: resp.statusCode,
939-
errors: errors,
940-
);
921+
status: resp.statusCode, errorCollection: errorCollection);
941922
}
942923
}
943924

@@ -955,15 +936,13 @@ class Api with Logging {
955936
}
956937
}
957938

958-
List<ApiError>? _extractErrors(List<dynamic>? data) {
959-
if (data == null) {
939+
ApiErrorCollection? _extractErrorCollection(Map<String, dynamic>? data) {
940+
if (data?[_kErrorsKey] == null) {
960941
return null;
961942
}
962943

963944
logSevere(data);
964-
return List<_JsonObject>.from(data)
965-
.map(ApiError.fromJson)
966-
.toList(growable: false);
945+
return ApiErrorCollection.fromJson(data);
967946
}
968947

969948
dynamic _ensureNotNullOrEmpty(dynamic param) {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class TokenCache {
7373
_clientId = clientId;
7474
}
7575

76+
/// Dispose of [TokenCache]
77+
void terminate() {}
78+
7679
/// Reset the [TokenCache]
7780
///
7881
void clear() {

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

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ class Auth {
3737
static const _kClientKey = '\$client';
3838
static const _kEnvKey = '\$env';
3939
static const _codeLength = 6;
40+
static const _defaultPollDelay = Duration(seconds: 53);
4041

4142
Timer? _clientTimer;
4243
Timer? _persistenceTimer;
4344
Timer? _refetchTimer;
45+
Timer? _pollTimer;
4446
Map<String, dynamic> _persistableData = {};
4547

4648
/// Stream of errors reported by the SDK of type [AuthError]
@@ -125,7 +127,7 @@ class Auth {
125127
Future<void> initialize() async {
126128
await config.initialize();
127129
telemetry = Telemetry(config: config);
128-
_api = Api(config: config, sessionTokenSink: _sessionTokens.sink);
130+
_api = Api(config: config);
129131
await _api.initialize();
130132

131133
final (client, env) = await _fetchClientAndEnv();
@@ -166,6 +168,10 @@ class Auth {
166168
},
167169
);
168170
}
171+
172+
if (config.sessionTokenPolling) {
173+
await _pollForSessionToken();
174+
}
169175
}
170176

171177
/// Disposal of the [Auth] object
@@ -174,6 +180,7 @@ class Auth {
174180
/// method, if that is mixed in e.g. in clerk_flutter
175181
///
176182
void terminate() {
183+
_pollTimer?.cancel();
177184
_clientTimer?.cancel();
178185
_persistenceTimer?.cancel();
179186
_refetchTimer?.cancel();
@@ -184,6 +191,31 @@ class Auth {
184191
config.terminate();
185192
}
186193

194+
Future<void> _pollForSessionToken() async {
195+
_pollTimer?.cancel();
196+
197+
Duration delay = _defaultPollDelay;
198+
199+
try {
200+
final sessionToken = await _api.updateSessionToken();
201+
if (sessionToken case SessionToken token) {
202+
_sessionTokens.add(token);
203+
delay = token.expiry.difference(DateTime.timestamp());
204+
}
205+
} on AuthError catch (error) {
206+
addError(error);
207+
} catch (error) {
208+
addError(
209+
AuthError(
210+
code: AuthErrorCode.noSessionTokenRetrieved,
211+
message: error.toString(),
212+
),
213+
);
214+
} finally {
215+
_pollTimer = Timer(delay, _pollForSessionToken);
216+
}
217+
}
218+
187219
Future<void> _retryFetchClientAndEnv(_) async {
188220
final (client, env) = await _fetchClientAndEnv();
189221
if (client.isNotEmpty && env.isNotEmpty) {
@@ -211,7 +243,7 @@ class Auth {
211243

212244
ApiResponse _housekeeping(ApiResponse resp) {
213245
if (resp.isError) {
214-
addError(AuthError(code: resp.authErrorCode, message: resp.errorMessage));
246+
addError(AuthError.from(resp.errorCollection));
215247
} else if (resp.client case Client client) {
216248
this.client = client;
217249
}
@@ -286,13 +318,19 @@ class Auth {
286318
final org = env.organization.isEnabled
287319
? organization ?? Organization.personal
288320
: null;
289-
final token = await _api.sessionToken(org, templateName);
321+
SessionToken? token = _api.sessionToken(org, templateName);
290322
if (token is! SessionToken) {
291-
throw const AuthError(
292-
message: 'No session token retrieved',
293-
code: AuthErrorCode.noSessionTokenRetrieved,
294-
);
323+
token = await _api.updateSessionToken(org, templateName);
324+
if (token is SessionToken) {
325+
_sessionTokens.add(token);
326+
} else {
327+
throw const AuthError(
328+
message: 'No session token retrieved',
329+
code: AuthErrorCode.noSessionTokenRetrieved,
330+
);
331+
}
295332
}
333+
296334
return token;
297335
}
298336

packages/clerk_auth/lib/src/clerk_auth/auth_error.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:clerk_auth/src/models/api/api_error.dart';
2+
13
/// Container for errors encountered during Clerk auth(entication|orization)
24
///
35
class AuthError implements Exception {
@@ -8,6 +10,10 @@ class AuthError implements Exception {
810
this.argument,
911
});
1012

13+
/// Construct from an [ApiErrorCollection]
14+
factory AuthError.from(ApiErrorCollection errors) =>
15+
AuthError(code: errors.authErrorCode, message: errors.errorMessage);
16+
1117
/// Error code
1218
final AuthErrorCode? code;
1319

packages/clerk_auth/lib/src/models/api/api_error.dart

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:clerk_auth/src/clerk_auth/auth_error.dart';
22
import 'package:clerk_auth/src/models/informative_to_string_mixin.dart';
3+
import 'package:collection/collection.dart';
34
import 'package:json_annotation/json_annotation.dart';
45
import 'package:meta/meta.dart';
56

@@ -38,10 +39,39 @@ class ApiError with InformativeToStringMixin {
3839
String get fullMessage => longMessage ?? message;
3940

4041
/// fromJson
41-
static ApiError fromJson(Map<String, dynamic> json) =>
42-
_$ApiErrorFromJson(json);
42+
static ApiError fromJson(dynamic json) {
43+
return _$ApiErrorFromJson(json as Map<String, dynamic>);
44+
}
4345

4446
/// toJson
4547
@override
4648
Map<String, dynamic> toJson() => _$ApiErrorToJson(this);
4749
}
50+
51+
/// [ApiErrorCollection] Clerk object
52+
@immutable
53+
@JsonSerializable()
54+
class ApiErrorCollection {
55+
/// Constructor
56+
const ApiErrorCollection({this.errors});
57+
58+
/// The [ApiError]s
59+
final List<ApiError>? errors;
60+
61+
/// formatted error message
62+
String get errorMessage =>
63+
errors?.map((e) => e.fullMessage).join('; ') ?? 'Unknown error';
64+
65+
/// First [AuthErrorCode] encountered
66+
AuthErrorCode get authErrorCode =>
67+
errors?.map((e) => e.authErrorCode).nonNulls.firstOrNull ??
68+
AuthErrorCode.serverErrorResponse;
69+
70+
/// fromJson
71+
static ApiErrorCollection fromJson(dynamic json) {
72+
return _$ApiErrorCollectionFromJson(json as Map<String, dynamic>);
73+
}
74+
75+
/// toJson
76+
Map<String, dynamic> toJson() => _$ApiErrorCollectionToJson(this);
77+
}

packages/clerk_auth/lib/src/models/api/api_error.g.dart

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)