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
5 changes: 4 additions & 1 deletion packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,10 @@ class GoTrueClient {
_onAuthStateChangeControllerSync.close();
_broadcastChannel?.close();
_broadcastChannelSubscription?.cancel();
_refreshTokenCompleter?.completeError(AuthException('Disposed'));
final completer = _refreshTokenCompleter;
if (completer != null && !completer.isCompleted) {
completer.completeError(AuthException('Disposed'));
}
_autoRefreshTicker?.cancel();
}

Expand Down
13 changes: 12 additions & 1 deletion packages/gotrue/lib/src/types/auth_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,18 @@ class GenerateLinkResponse {

GenerateLinkResponse.fromJson(Map<String, dynamic> json)
: properties = GenerateLinkProperties.fromJson(json),
user = User.fromJson(json)!;
user = _parseUser(json);

static User _parseUser(Map<String, dynamic> json) {
final user = User.fromJson(json);
if (user == null) {
throw FormatException(
'Failed to parse user: missing required id field',
json.toString(),
);
}
return user;
}
}

class GenerateLinkProperties {
Expand Down
133 changes: 108 additions & 25 deletions packages/gotrue/lib/src/types/mfa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ class AuthMFAEnrollResponse {
});

factory AuthMFAEnrollResponse.fromJson(Map<String, dynamic> json) {
final type = FactorType.values.firstWhere((e) => e.name == json['type']);
final type = FactorType.values.firstWhere(
(e) => e.name == json['type'],
orElse: () => FactorType.unknown,
);
return AuthMFAEnrollResponse(
id: json['id'],
id: json['id'] as String,
type: type,
totp: type == FactorType.totp && json['totp'] != null
? TOTPEnrollment.fromJson(json['totp'])
? TOTPEnrollment.fromJson(json['totp'] as Map<String, dynamic>)
: null,
phone: type == FactorType.phone && json['phone'] != null
? PhoneEnrollment._fromJsonValue(json['phone'])
Expand Down Expand Up @@ -57,9 +60,9 @@ class TOTPEnrollment {

factory TOTPEnrollment.fromJson(Map<String, dynamic> json) {
return TOTPEnrollment(
qrCode: json['qr_code'],
secret: json['secret'],
uri: json['uri'],
qrCode: json['qr_code'] as String,
secret: json['secret'] as String,
uri: json['uri'] as String,
);
}
}
Expand All @@ -74,7 +77,7 @@ class PhoneEnrollment {

factory PhoneEnrollment.fromJson(Map<String, dynamic> json) {
return PhoneEnrollment(
phone: json['phone'],
phone: json['phone'] as String,
);
}

Expand Down Expand Up @@ -102,9 +105,17 @@ class AuthMFAChallengeResponse {
const AuthMFAChallengeResponse({required this.id, required this.expiresAt});

factory AuthMFAChallengeResponse.fromJson(Map<String, dynamic> json) {
final expiresAtValue = json['expires_at'];
if (expiresAtValue is! num) {
throw FormatException(
'Expected expires_at to be a number, got ${expiresAtValue.runtimeType}',
json.toString(),
);
}
final expiresAt = expiresAtValue.toInt();
return AuthMFAChallengeResponse(
id: json['id'],
expiresAt: DateTime.fromMillisecondsSinceEpoch(json['expires_at'] * 1000),
id: json['id'] as String,
expiresAt: DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000),
);
}
}
Expand Down Expand Up @@ -134,12 +145,34 @@ class AuthMFAVerifyResponse {
});

factory AuthMFAVerifyResponse.fromJson(Map<String, dynamic> json) {
final expiresInValue = json['expires_in'];
if (expiresInValue is! num) {
throw FormatException(
'Expected expires_in to be a number, got ${expiresInValue.runtimeType}',
json.toString(),
);
}
final expiresIn = expiresInValue.toInt();
final userJson = json['user'];
if (userJson is! Map<String, dynamic>) {
throw FormatException(
'Expected user to be an object, got ${userJson.runtimeType}',
json.toString(),
);
}
final user = User.fromJson(userJson);
if (user == null) {
throw FormatException(
'Failed to parse user object: missing required fields',
json.toString(),
);
}
return AuthMFAVerifyResponse(
accessToken: json['access_token'],
tokenType: json['token_type'],
expiresIn: Duration(seconds: json['expires_in']),
refreshToken: json['refresh_token'],
user: User.fromJson(json['user'])!,
accessToken: json['access_token'] as String,
tokenType: json['token_type'] as String,
expiresIn: Duration(seconds: expiresIn),
refreshToken: json['refresh_token'] as String,
user: user,
);
}
}
Expand All @@ -151,7 +184,7 @@ class AuthMFAUnenrollResponse {
const AuthMFAUnenrollResponse({required this.id});

factory AuthMFAUnenrollResponse.fromJson(Map<String, dynamic> json) {
return AuthMFAUnenrollResponse(id: json['id']);
return AuthMFAUnenrollResponse(id: json['id'] as String);
}
}

Expand All @@ -174,9 +207,17 @@ class AuthMFAAdminListFactorsResponse {
const AuthMFAAdminListFactorsResponse({required this.factors});

factory AuthMFAAdminListFactorsResponse.fromJson(Map<String, dynamic> json) {
final factorsList = json['factors'];
if (factorsList is! List) {
throw FormatException(
'Expected factors to be a list, got ${factorsList.runtimeType}',
json.toString(),
);
}
return AuthMFAAdminListFactorsResponse(
factors:
(json['factors'] as List).map((e) => Factor.fromJson(e)).toList(),
factors: factorsList
.map((e) => Factor.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
Expand All @@ -188,13 +229,27 @@ class AuthMFAAdminDeleteFactorResponse {
const AuthMFAAdminDeleteFactorResponse({required this.id});

factory AuthMFAAdminDeleteFactorResponse.fromJson(Map<String, dynamic> json) {
return AuthMFAAdminDeleteFactorResponse(id: json['id']);
return AuthMFAAdminDeleteFactorResponse(id: json['id'] as String);
}
}

enum FactorStatus { verified, unverified }
enum FactorStatus {
verified,
unverified,

enum FactorType { totp, phone }
/// Returned when the backend sends an unknown status value.
/// This allows forward compatibility with new status types.
unknown,
}

enum FactorType {
totp,
phone,

/// Returned when the backend sends an unknown factor type.
/// This allows forward compatibility with new factor types.
unknown,
}

class Factor {
/// ID of the factor.
Expand Down Expand Up @@ -222,17 +277,37 @@ class Factor {
});

factory Factor.fromJson(Map<String, dynamic> json) {
DateTime parseDateTime(String key) {
final value = json[key];
if (value is! String) {
throw FormatException(
'Expected $key to be a string, got ${value.runtimeType}',
json.toString(),
);
}
try {
return DateTime.parse(value);
} on FormatException {
throw FormatException(
'Invalid date format for $key: $value',
json.toString(),
);
}
}

return Factor(
id: json['id'],
friendlyName: json['friendly_name'],
id: json['id'] as String,
friendlyName: json['friendly_name'] as String?,
factorType: FactorType.values.firstWhere(
(e) => e.name == json['factor_type'],
orElse: () => FactorType.unknown,
),
status: FactorStatus.values.firstWhere(
(e) => e.name == json['status'],
orElse: () => FactorStatus.unknown,
),
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
createdAt: parseDateTime('created_at'),
updatedAt: parseDateTime('updated_at'),
);
}

Expand Down Expand Up @@ -337,12 +412,20 @@ class AMREntry {
const AMREntry({required this.method, required this.timestamp});

factory AMREntry.fromJson(Map<String, dynamic> json) {
final timestampValue = json['timestamp'];
if (timestampValue is! num) {
throw FormatException(
'Expected timestamp to be a number, got ${timestampValue.runtimeType}',
json.toString(),
);
}
final timestamp = timestampValue.toInt();
return AMREntry(
method: AMRMethod.values.firstWhere(
(e) => e.code == json['method'],
orElse: () => AMRMethod.unknown,
),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] * 1000),
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
);
}
}
14 changes: 13 additions & 1 deletion packages/gotrue/lib/src/types/session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,26 @@ class Session {
if (json['access_token'] == null) {
return null;
}
final userJson = json['user'];
if (userJson is! Map<String, dynamic>) {
throw FormatException(
'Expected user to be an object, got ${userJson.runtimeType}',
);
}
final user = User.fromJson(userJson);
if (user == null) {
throw FormatException(
'Failed to parse user: missing required id field',
);
}
return Session(
accessToken: json['access_token'] as String,
expiresIn: json['expires_in'] as int?,
refreshToken: json['refresh_token'] as String?,
tokenType: json['token_type'] as String,
providerToken: json['provider_token'] as String?,
providerRefreshToken: json['provider_refresh_token'] as String?,
user: User.fromJson(json['user'] as Map<String, dynamic>)!,
user: user,
);
}

Expand Down
16 changes: 12 additions & 4 deletions packages/postgrest/lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,19 @@ class PostgrestResponse<T> {

final int count;

factory PostgrestResponse.fromJson(Map<String, dynamic> json) =>
PostgrestResponse<T>(
data: json['data'] as T,
count: json['count'] as int,
factory PostgrestResponse.fromJson(Map<String, dynamic> json) {
final countValue = json['count'];
if (countValue is! num) {
throw FormatException(
'Expected count to be a number, got ${countValue.runtimeType}',
json.toString(),
);
}
return PostgrestResponse<T>(
data: json['data'] as T,
count: countValue.toInt(),
);
}

Map<String, dynamic> toJson() => {
'data': data,
Expand Down
17 changes: 14 additions & 3 deletions packages/realtime_client/lib/src/realtime_presence.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,20 @@ class Presence {
/// The payload shared by users.
final Map<String, dynamic> payload;

Presence.fromJson(Map<String, dynamic> map)
: presenceRef = map['presence_ref'],
payload = map..remove('presence_ref');
const Presence({
required this.presenceRef,
required this.payload,
});

factory Presence.fromJson(Map<String, dynamic> map) {
final ref = map['presence_ref'];
// Create a new map without presence_ref to avoid mutating the input
final payload = Map<String, dynamic>.from(map)..remove('presence_ref');
return Presence(
presenceRef: ref as String? ?? '',
payload: payload,
);
}

Presence deepClone() {
return Presence.fromJson({
Expand Down
38 changes: 29 additions & 9 deletions packages/realtime_client/lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,35 @@ class PostgresChangePayload {
});

/// Creates a PostgresChangePayload instance from the enriched postgres change payload
PostgresChangePayload.fromPayload(Map<String, dynamic> payload)
: schema = payload['schema'],
table = payload['table'],
commitTimestamp =
DateTime.parse(payload['commit_timestamp'] ?? '19700101'),
eventType = PostgresChangeEventMethods.fromString(payload['eventType']),
newRecord = Map<String, dynamic>.from(payload['new']),
oldRecord = Map<String, dynamic>.from(payload['old']),
errors = payload['errors'];
factory PostgresChangePayload.fromPayload(Map<String, dynamic> payload) {
final commitTimestampStr = payload['commit_timestamp'] as String?;
DateTime commitTimestamp;
try {
commitTimestamp = commitTimestampStr != null
? DateTime.parse(commitTimestampStr)
: DateTime.fromMillisecondsSinceEpoch(0);
} on FormatException {
commitTimestamp = DateTime.fromMillisecondsSinceEpoch(0);
}

final newData = payload['new'];
final oldData = payload['old'];

return PostgresChangePayload(
schema: payload['schema'] as String,
table: payload['table'] as String,
commitTimestamp: commitTimestamp,
eventType:
PostgresChangeEventMethods.fromString(payload['eventType'] as String),
newRecord: newData is Map
? Map<String, dynamic>.from(newData)
: <String, dynamic>{},
oldRecord: oldData is Map
? Map<String, dynamic>.from(oldData)
: <String, dynamic>{},
errors: payload['errors'],
);
}

@override
String toString() {
Expand Down
Loading
Loading