Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 23 additions & 48 deletions packages/clerk_auth/lib/src/clerk_api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class Api with Logging {
///
Api({
required this.config,
this.sessionTokenSink,
}) : _tokenCache = TokenCache(
persistor: config.persistor,
publishableKey: config.publishableKey,
Expand All @@ -36,14 +35,10 @@ class Api with Logging {
/// The config used to initialize this api instance.
final AuthConfig config;

/// The [Sink] for session tokens
final Sink<SessionToken>? sessionTokenSink;

final TokenCache _tokenCache;
final String _domain;

bool _testMode;
Timer? _pollTimer;
bool _multiSessionMode = true;

static const _kClerkAPIVersion = 'clerk-api-version';
Expand All @@ -61,19 +56,14 @@ class Api with Logging {
static const _kXMobile = 'x-mobile';
static const _scheme = 'https';

static const _defaultPollDelay = Duration(seconds: 53);

/// Initialise the API
Future<void> initialize() async {
await _tokenCache.initialize();
if (config.sessionTokenPollMode == SessionTokenPollMode.hungry) {
await _pollForSessionToken();
}
}

/// Dispose of the API
void terminate() {
_pollTimer?.cancel();
_tokenCache.terminate();
}

/// Confirm connectivity to the back end
Expand Down Expand Up @@ -786,18 +776,15 @@ class Api with Logging {

// Session

/// Return the [SessionToken] for the current active [Session], refreshing it
/// if required
/// Return the [SessionToken] for the current active [Session], if
/// available
///
Future<SessionToken?> sessionToken([
Organization? org,
String? templateName,
]) async {
return _tokenCache.sessionTokenFor(org, templateName) ??
await _updateSessionToken(org, templateName);
}
SessionToken? sessionToken([Organization? org, String? templateName]) =>
_tokenCache.sessionTokenFor(org, templateName);

Future<SessionToken?> _updateSessionToken([
/// Refresh and return the [SessionToken] for the current active [Session]
///
Future<SessionToken?> updateSessionToken([
Organization? org,
String? templateName,
]) async {
Expand All @@ -816,30 +803,22 @@ class Api with Logging {
_kOrganizationId: org.externalId,
},
);
final body = json.decode(resp.body) as _JsonObject;
if (resp.statusCode == HttpStatus.ok) {
final body = json.decode(resp.body) as _JsonObject;
final token = body[_kJwtKey] as String;
final sessionToken =
_tokenCache.makeAndCacheSessionToken(token, templateName);
sessionTokenSink?.add(sessionToken);
return sessionToken;
return _tokenCache.makeAndCacheSessionToken(token, templateName);
} else if (_extractErrorCollection(body) case ApiErrorCollection errors) {
throw AuthError.from(errors);
} else {
throw const AuthError(
message: 'No session token retrieved',
code: AuthErrorCode.noSessionTokenRetrieved,
);
}
}
return null;
}

Future<void> _pollForSessionToken() async {
_pollTimer?.cancel();

final sessionToken = await _updateSessionToken();
final delay = switch (sessionToken) {
SessionToken sessionToken when sessionToken.isNotExpired =>
sessionToken.expiry.difference(DateTime.timestamp()),
_ => _defaultPollDelay,
};
_pollTimer = Timer(delay, _pollForSessionToken);
}

// Internal

Future<ApiResponse> _uploadFile(HttpMethod method, Uri uri, File file) async {
Expand Down Expand Up @@ -897,22 +876,20 @@ class Api with Logging {

ApiResponse _processResponse(http.Response resp) {
final body = json.decode(resp.body) as _JsonObject;
final errors = _extractErrors(body[_kErrorsKey]);
final errorCollection = _extractErrorCollection(body);
final (clientData, responseData) = _extractClientAndResponse(body);
if (clientData is _JsonObject) {
final client = Client.fromJson(clientData);
_tokenCache.updateFrom(resp, client);
return ApiResponse(
client: client,
status: resp.statusCode,
errors: errors,
errorCollection: errorCollection,
response: responseData,
);
} else {
return ApiResponse(
status: resp.statusCode,
errors: errors,
);
status: resp.statusCode, errorCollection: errorCollection);
}
}

Expand All @@ -930,15 +907,13 @@ class Api with Logging {
}
}

List<ApiError>? _extractErrors(List<dynamic>? data) {
if (data == null) {
ApiErrorCollection? _extractErrorCollection(Map<String, dynamic>? data) {
if (data?[_kErrorsKey] == null) {
return null;
}

logSevere(data);
return List<_JsonObject>.from(data)
.map(ApiError.fromJson)
.toList(growable: false);
return ApiErrorCollection.fromJson(data);
}

Future<http.Response> _fetch({
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk_auth/lib/src/clerk_api/token_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ class TokenCache {
_clientId = clientId;
}

/// Dispose of [TokenCache]
void terminate() {}

/// Reset the [TokenCache]
///
void clear() {
Expand Down
52 changes: 45 additions & 7 deletions packages/clerk_auth/lib/src/clerk_auth/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ class Auth {
static const _kClientKey = '\$client';
static const _kEnvKey = '\$env';
static const _codeLength = 6;
static const _defaultPollDelay = Duration(seconds: 53);

Timer? _clientTimer;
Timer? _persistenceTimer;
Timer? _refetchTimer;
Timer? _pollTimer;
Map<String, dynamic> _persistableData = {};

/// Stream of errors reported by the SDK of type [AuthError]
Expand Down Expand Up @@ -122,7 +124,7 @@ class Auth {
Future<void> initialize() async {
await config.initialize();
telemetry = Telemetry(config: config);
_api = Api(config: config, sessionTokenSink: _sessionTokens.sink);
_api = Api(config: config);
await _api.initialize();

final (client, env) = await _fetchClientAndEnv();
Expand Down Expand Up @@ -163,6 +165,10 @@ class Auth {
},
);
}

if (config.sessionTokenPollMode == SessionTokenPollMode.hungry) {
await _pollForSessionToken();
}
}

/// Disposal of the [Auth] object
Expand All @@ -171,6 +177,7 @@ class Auth {
/// method, if that is mixed in e.g. in clerk_flutter
///
void terminate() {
_pollTimer?.cancel();
_clientTimer?.cancel();
_persistenceTimer?.cancel();
_refetchTimer?.cancel();
Expand All @@ -181,6 +188,31 @@ class Auth {
config.terminate();
}

Future<void> _pollForSessionToken() async {
_pollTimer?.cancel();

Duration delay = _defaultPollDelay;

try {
final sessionToken = await _api.updateSessionToken();
if (sessionToken case SessionToken token) {
_sessionTokens.add(token);
delay = token.expiry.difference(DateTime.timestamp());
}
} on AuthError catch (error) {
addError(error);
} catch (error) {
addError(
AuthError(
code: AuthErrorCode.noSessionTokenRetrieved,
message: error.toString(),
),
);
} finally {
_pollTimer = Timer(delay, _pollForSessionToken);
}
}

Future<void> _retryFetchClientAndEnv(_) async {
final (client, env) = await _fetchClientAndEnv();
if (client.isNotEmpty && env.isNotEmpty) {
Expand Down Expand Up @@ -208,7 +240,7 @@ class Auth {

ApiResponse _housekeeping(ApiResponse resp) {
if (resp.isError) {
addError(AuthError(code: resp.authErrorCode, message: resp.errorMessage));
addError(AuthError.from(resp.errorCollection));
} else if (resp.client case Client client) {
this.client = client;
}
Expand Down Expand Up @@ -268,13 +300,19 @@ class Auth {
final org = env.organization.isEnabled
? organization ?? Organization.personal
: null;
final token = await _api.sessionToken(org, templateName);
SessionToken? token = _api.sessionToken(org, templateName);
if (token is! SessionToken) {
throw const AuthError(
message: 'No session token retrieved',
code: AuthErrorCode.noSessionTokenRetrieved,
);
token = await _api.updateSessionToken(org, templateName);
if (token is SessionToken) {
_sessionTokens.add(token);
} else {
throw const AuthError(
message: 'No session token retrieved',
code: AuthErrorCode.noSessionTokenRetrieved,
);
}
}

return token;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/clerk_auth/lib/src/clerk_auth/auth_error.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:clerk_auth/src/models/api/api_error.dart';

/// Container for errors encountered during Clerk auth(entication|orization)
///
class AuthError implements Exception {
Expand All @@ -8,6 +10,10 @@ class AuthError implements Exception {
this.argument,
});

/// Construct from an [ApiErrorCollection]
factory AuthError.from(ApiErrorCollection errors) =>
AuthError(code: errors.authErrorCode, message: errors.errorMessage);

/// Error code
final AuthErrorCode? code;

Expand Down
34 changes: 32 additions & 2 deletions packages/clerk_auth/lib/src/models/api/api_error.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:clerk_auth/src/clerk_auth/auth_error.dart';
import 'package:clerk_auth/src/models/informative_to_string_mixin.dart';
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

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

/// fromJson
static ApiError fromJson(Map<String, dynamic> json) =>
_$ApiErrorFromJson(json);
static ApiError fromJson(dynamic json) {
return _$ApiErrorFromJson(json as Map<String, dynamic>);
}

/// toJson
@override
Map<String, dynamic> toJson() => _$ApiErrorToJson(this);
}

/// [ApiErrorCollection] Clerk object
@immutable
@JsonSerializable()
class ApiErrorCollection {
/// Constructor
const ApiErrorCollection({this.errors});

/// The [ApiError]s
final List<ApiError>? errors;

/// formatted error message
String get errorMessage =>
errors?.map((e) => e.fullMessage).join('; ') ?? 'Unknown error';

/// First [AuthErrorCode] encountered
AuthErrorCode get authErrorCode =>
errors?.map((e) => e.authErrorCode).nonNulls.firstOrNull ??
AuthErrorCode.serverErrorResponse;

/// fromJson
static ApiErrorCollection fromJson(dynamic json) {
return _$ApiErrorCollectionFromJson(json as Map<String, dynamic>);
}

/// toJson
Map<String, dynamic> toJson() => _$ApiErrorCollectionToJson(this);
}
20 changes: 20 additions & 0 deletions packages/clerk_auth/lib/src/models/api/api_error.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading