Skip to content

Commit 7bf65ed

Browse files
fix: improve multilingual support [#128] (#132)
* fix: improve multilingual support [#128] * fix: reformat generated file to meet flutter standards [#128] * fix: reformat en localizations too [#128] * fix: delete clerk_translator class [#128] * feat: add localization of internal error messages [#128] * feat: add accept-language header to http requests [#128] * fix: slight refactor to stop empty error being throw [#128] * fix: minor rewording [#128] * fix: make autherror argument string only [#128] * fix: formatting in generated files [#128] * fix: pr changes --------- Co-authored-by: Simon Lightfoot <[email protected]>
1 parent 31f4507 commit 7bf65ed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1947
-588
lines changed

.run/Flutter Run -_ 'clerk_flutter_example'.run.xml .run/Clerk Flutter Example.run.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<component name="ProjectRunConfigurationManager">
2-
<configuration default="false" name="Flutter Run -&gt; 'clerk_flutter_example'" type="FlutterRunConfigurationType" factoryName="Flutter">
3-
<option name="additionalArgs" value="--dart-define-from-file=example.env" />
2+
<configuration default="false" name="Clerk Flutter Example" type="FlutterRunConfigurationType" factoryName="Flutter">
3+
<option name="additionalArgs" value="--dart-define-from-file=example.json" />
44
<option name="filePath" value="$PROJECT_DIR$/packages/clerk_flutter/example/lib/main.dart" />
55
<method v="2" />
66
</configuration>

.run/Flutter Run -_ 'clerk_widgetbook'.run.xml

-6
This file was deleted.

melos.yaml

+13-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,16 @@ scripts:
3939

4040
# Run build and format code
4141
brunner_format:
42-
run: melos brunner && melos format
42+
run: melos run brunner && melos run format
43+
44+
# Run localizations
45+
localization:
46+
run: melos run sort_localizations && melos exec -- fvm flutter gen-l10n && melos run format
47+
packageFilters:
48+
scope: "clerk_flutter"
49+
50+
# Sort localizations `dart pub global activate arb_utils`
51+
sort_localizations:
52+
run: melos exec -- arb_utils sort l10n/en.arb
53+
packageFilters:
54+
scope: "clerk_flutter"

packages/clerk_auth/example/main.dart

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ Future<void> main() async {
88
persistor: await DefaultPersistor.create(
99
storageDirectory: Directory.current,
1010
),
11-
httpService: const DefaultHttpService(),
12-
pollMode: SessionTokenPollMode.lazy,
1311
);
1412

1513
Client client;

packages/clerk_auth/lib/clerk_auth.dart

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
library clerk_auth;
33

44
export 'src/clerk_api/telemetry.dart';
5+
export 'src/clerk_api/api.dart' show ClerkLocalesLookup;
56
export 'src/clerk_auth/auth.dart';
67
export 'src/clerk_constants.dart';
78
export 'src/models/models.dart';

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@ import 'package:http/http.dart' as http;
1515

1616
export 'package:clerk_auth/src/models/enums.dart' show SessionTokenPollMode;
1717

18+
/// Used by [Api] to locate the current user locale preference.
19+
typedef ClerkLocalesLookup = List<String> Function();
20+
1821
/// [Api] manages communication with the Clerk frontend API
1922
///
2023
class Api with Logging {
21-
Api._(this._tokenCache, this._domain, this._httpService, this._pollMode);
24+
Api._(
25+
this._tokenCache,
26+
this._domain,
27+
this._httpService,
28+
this._localesLookup,
29+
this._pollMode,
30+
);
2231

2332
/// Create an [Api] object for a given Publishable Key, or return the existing one
2433
/// if such already exists for that key. Requires a [publishableKey]
@@ -37,6 +46,7 @@ class Api with Logging {
3746
required String publishableKey,
3847
required Persistor persistor,
3948
required HttpService httpService,
49+
required ClerkLocalesLookup localesLookup,
4050
SessionTokenPollMode pollMode = SessionTokenPollMode.lazy,
4151
}) =>
4252
Api._(
@@ -46,12 +56,14 @@ class Api with Logging {
4656
),
4757
_deriveDomainFrom(publishableKey),
4858
httpService,
59+
localesLookup,
4960
pollMode,
5061
);
5162

5263
final TokenCache _tokenCache;
5364
final String _domain;
5465
final HttpService _httpService;
66+
final ClerkLocalesLookup _localesLookup;
5567
final SessionTokenPollMode _pollMode;
5668
late final String _nativeDeviceId;
5769
Timer? _pollTimer;
@@ -878,6 +890,7 @@ class Api with Logging {
878890
}) {
879891
return {
880892
HttpHeaders.acceptHeader: 'application/json',
893+
HttpHeaders.acceptLanguageHeader: _localesLookup().join(', '),
881894
HttpHeaders.contentTypeHeader: method.isGet
882895
? 'application/json'
883896
: 'application/x-www-form-urlencoded',

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

+22-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Auth {
4646
required Persistor persistor,
4747
bool sendTelemetryData = true,
4848
HttpService httpService = const DefaultHttpService(),
49+
ClerkLocalesLookup localesLookup = defaultLocalesList,
4950
SessionTokenPollMode pollMode = SessionTokenPollMode.lazy,
5051
}) : telemetry = Telemetry(
5152
publishableKey: publishableKey,
@@ -57,9 +58,13 @@ class Auth {
5758
publishableKey: publishableKey,
5859
persistor: persistor,
5960
httpService: httpService,
61+
localesLookup: localesLookup,
6062
pollMode: pollMode,
6163
);
6264

65+
/// Use 'English' as the default locale
66+
static List<String> defaultLocalesList() => <String>['en'];
67+
6368
/// The service to send telemetry to the back end
6469
final Telemetry telemetry;
6570

@@ -151,10 +156,14 @@ class Auth {
151156
}
152157

153158
ApiResponse _housekeeping(ApiResponse resp) {
154-
if (resp.client case Client client when resp.isOkay) {
159+
if (resp.isError) {
160+
throw AuthError(
161+
code: AuthErrorCode.serverErrorResponse,
162+
message: '{arg}: ${resp.errorMessage}',
163+
argument: resp.status.toString(),
164+
);
165+
} else if (resp.client case Client client) {
155166
this.client = client;
156-
} else {
157-
throw AuthError(code: resp.status, message: resp.errorMessage);
158167
}
159168
return resp;
160169
}
@@ -185,7 +194,10 @@ class Auth {
185194
: null;
186195
final token = await _api.sessionToken(org, templateName);
187196
if (token is! SessionToken) {
188-
throw AuthError(message: 'No session token retrieved');
197+
throw const AuthError(
198+
message: 'No session token retrieved',
199+
code: AuthErrorCode.noSessionTokenRetrieved,
200+
);
189201
}
190202
return token;
191203
}
@@ -339,7 +351,10 @@ class Auth {
339351
String? signature,
340352
}) async {
341353
if (password != passwordConfirmation) {
342-
throw AuthError(message: "Password and password confirmation must match");
354+
throw const AuthError(
355+
message: "Password and password confirmation must match",
356+
code: AuthErrorCode.passwordMatchError,
357+
);
343358
}
344359

345360
switch (client.signUp) {
@@ -534,8 +549,9 @@ class Auth {
534549

535550
final expiry = client.signIn?.firstFactorVerification?.expireAt;
536551
if (expiry?.isAfter(DateTime.timestamp()) != true) {
537-
throw AuthError(
552+
throw const AuthError(
538553
message: 'Awaited user action not completed in required timeframe',
554+
code: AuthErrorCode.actionNotTimely,
539555
);
540556
}
541557

Original file line numberDiff line numberDiff line change
@@ -1,27 +1,69 @@
11
/// Container for errors encountered during Clerk auth(entication|orization)
22
///
3-
class AuthError extends Error {
3+
class AuthError implements Exception {
44
/// Construct an [AuthError]
5-
AuthError({this.code, required this.message, this.substitutions});
5+
const AuthError({
6+
required this.code,
7+
required this.message,
8+
this.argument,
9+
});
610

7-
/// An error [code], likely to be an http status code
8-
final int? code;
11+
/// Error code
12+
final AuthErrorCode? code;
913

1014
/// The associated [message]
1115
final String message;
1216

13-
/// A possible [substitution] within the message
14-
final List<dynamic>? substitutions;
17+
/// Any arguments
18+
final String? argument;
1519

1620
@override
1721
String toString() {
18-
if (substitutions case List<dynamic> subs when subs.isNotEmpty) {
19-
String msg = message.replaceFirst('###', subs.first);
20-
for (int i = 1; i < subs.length; ++i) {
21-
msg = msg.replaceFirst('#${i + 1}#', subs[i].toString());
22-
}
23-
return msg;
22+
if (argument case String argument) {
23+
return message.replaceFirst('{arg}', argument);
2424
}
2525
return message;
2626
}
2727
}
28+
29+
/// Code to enable consuming apps to identify the error
30+
enum AuthErrorCode {
31+
/// Server error response
32+
serverErrorResponse,
33+
34+
/// Error during sign-up flow
35+
signUpFlowError,
36+
37+
/// Invalid Password
38+
invalidPassword,
39+
40+
/// Type Invalid
41+
typeInvalid,
42+
43+
/// No stage for status
44+
noStageForStatus,
45+
46+
/// No session token retrieved
47+
noSessionTokenRetrieved,
48+
49+
/// No strategy associated with type,
50+
noAssociatedStrategy,
51+
52+
/// Password and password confirmation must match
53+
passwordMatchError,
54+
55+
/// JWT poorly formatted
56+
jwtPoorlyFormatted,
57+
58+
/// Awaited user action not completed in required timeframe
59+
actionNotTimely,
60+
61+
/// No session found for user
62+
noSessionFoundForUser,
63+
64+
/// Unsupported strategy for first factor
65+
noSuchFirstFactorStrategy,
66+
67+
/// Unsupported strategy for second factor
68+
noSuchSecondFactorStrategy,
69+
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,11 @@ class Client {
8080
return session;
8181
}
8282
}
83-
throw AuthError(message: 'No session found for ${user.name}');
83+
throw AuthError(
84+
message: 'No session found for {arg}',
85+
argument: user.name,
86+
code: AuthErrorCode.noSessionFoundForUser,
87+
);
8488
}
8589

8690
/// Get the latest version of this [User]

packages/clerk_auth/lib/src/models/client/field.dart

+4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ class Field {
2323
/// email address
2424
static const emailAddress = Field._(name: 'email_address');
2525

26+
/// username
27+
static const username = Field._(name: 'username');
28+
2629
static final _values = <String, Field>{
2730
phoneNumber.name: phoneNumber,
2831
emailAddress.name: emailAddress,
32+
username.name: username,
2933
};
3034

3135
/// The [values] of the Fields

packages/clerk_auth/lib/src/models/client/session_token.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ class SessionToken {
2525

2626
late final _parts = switch (jwt.split('.')) {
2727
List<String> parts when parts.length == 3 => parts,
28-
_ => throw AuthError(message: "JWT poorly formatted: $jwt"),
28+
_ => throw AuthError(
29+
message: "JWT poorly formatted: {arg}",
30+
argument: jwt,
31+
code: AuthErrorCode.jwtPoorlyFormatted,
32+
),
2933
};
3034

3135
/// The [header] of the token

packages/clerk_auth/lib/src/models/client/sign_in.dart

+14-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,19 @@ class SignIn {
9494
for (final factor in factors) {
9595
if (factor.strategy == strategy) return factor;
9696
}
97-
throw AuthError(
98-
message: 'Strategy $strategy unsupported for $stage factor');
97+
switch (stage) {
98+
case Stage.first:
99+
throw AuthError(
100+
message: 'Strategy {arg} unsupported for first factor',
101+
argument: strategy.toString(),
102+
code: AuthErrorCode.noSuchFirstFactorStrategy,
103+
);
104+
case Stage.second:
105+
throw AuthError(
106+
message: 'Strategy {arg} unsupported for second factor',
107+
argument: strategy.toString(),
108+
code: AuthErrorCode.noSuchSecondFactorStrategy,
109+
);
110+
}
99111
}
100112
}

packages/clerk_auth/lib/src/models/client/strategy.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ class Strategy {
199199
'phone_number' => Strategy.phoneCode,
200200
'email_address' => Strategy.emailCode,
201201
String name => throw AuthError(
202-
message: 'No strategy associated with ### \'#2#\'',
203-
substitutions: [T.runtimeType, name],
202+
message: 'No strategy associated with {arg}',
203+
argument: '${T.runtimeType} \'$name\'',
204+
code: AuthErrorCode.noAssociatedStrategy,
204205
),
205206
};
206207
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,9 @@ enum Stage {
168168
Status.needsFirstFactor => first,
169169
Status.needsSecondFactor => second,
170170
_ => throw AuthError(
171-
message: 'No Stage for ###',
172-
substitutions: [status],
171+
message: 'No Stage for {arg}',
172+
argument: status.toString(),
173+
code: AuthErrorCode.noStageForStatus,
173174
),
174175
};
175176
}

packages/clerk_auth/test/integration/clerk_api/environment_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ void main() {
1717
publishableKey: env.publishableKey,
1818
persistor: Persistor.none,
1919
httpService: httpService,
20+
localesLookup: testLocalesLookup,
2021
pollMode: SessionTokenPollMode.lazy,
2122
);
2223
await api.initialize();

packages/clerk_auth/test/integration/clerk_api/sign_in_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ void main() {
2424
publishableKey: env.publishableKey,
2525
persistor: Persistor.none,
2626
httpService: httpService,
27+
localesLookup: testLocalesLookup,
2728
pollMode: SessionTokenPollMode.lazy,
2829
);
2930
await api.initialize();

packages/clerk_auth/test/integration/clerk_api/sign_up_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ void main() {
4040
publishableKey: env.publishableKey,
4141
persistor: Persistor.none,
4242
httpService: httpService,
43+
localesLookup: testLocalesLookup,
4344
pollMode: SessionTokenPollMode.lazy,
4445
);
4546
await api.initialize();

packages/clerk_auth/test/integration/clerk_api/user_details_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ void main() {
3232
publishableKey: env.publishableKey,
3333
persistor: Persistor.none,
3434
httpService: httpService,
35+
localesLookup: testLocalesLookup,
3536
pollMode: SessionTokenPollMode.lazy,
3637
);
3738
await api.initialize();

packages/clerk_auth/test/test_helpers.dart

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class TestHttpService implements HttpService {
6767
) {
6868
final hdrs = {...?headers}
6969
..remove(HttpHeaders.acceptHeader)
70+
..remove(HttpHeaders.acceptLanguageHeader)
7071
..remove(HttpHeaders.contentTypeHeader)
7172
..remove(HttpHeaders.authorizationHeader)
7273
..remove('clerk-api-version')
@@ -122,3 +123,5 @@ class TestHttpServiceError extends Error {
122123
@override
123124
String toString() => '$runtimeType: $message';
124125
}
126+
127+
List<String> testLocalesLookup() => <String>['en'];

0 commit comments

Comments
 (0)