Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
96640e6
fix: make google-one-tap work [#207]
shinyford Jul 29, 2025
5b7e91f
fix: remove reference to gcp project ID
shinyford Jul 29, 2025
c7f68dc
fix: remove oauth_token_google strategy [#207]
shinyford Jul 29, 2025
df6c664
fix: remove mention of googleonetap outside comms with the api [#207]
shinyford Aug 13, 2025
6d0bad2
fix: remove mention of google one tap in example also [#207]
shinyford Aug 13, 2025
552ad5e
fix: upgrade android build to non-imperative gradle [#207]
shinyford Aug 13, 2025
8ac5b9c
fix: refactor api and auth [#207]
shinyford Aug 13, 2025
737e2b2
fix: try upgrade to melos version [#207]
shinyford Aug 13, 2025
d569aa5
fix: update dart version in yaml files [#207]
shinyford Aug 13, 2025
041502f
fix: reformat [#207]
shinyford Aug 13, 2025
3cc34b6
fix: reformat localizations again [#207]
shinyford Aug 13, 2025
45eda14
fix: change comment and order of functions in strategy [#207]
shinyford Aug 18, 2025
482adc9
fix: reformat to 80 columns [#207]
shinyford Aug 18, 2025
190399b
fix: reformat to 80 columns [#207]
shinyford Aug 18, 2025
75b5454
fix: revert withalpha to withopacity [#207]
shinyford Aug 18, 2025
fd2d3b1
fix: revert to materialstateproperty [#207]
shinyford Aug 18, 2025
4aa49c0
fix: up flutter and dart versions in readme [#207]
shinyford Aug 18, 2025
9b4e8e8
fix: add documentation regarding env vars for the example app [#207]
shinyford Aug 18, 2025
6b963a7
fix: reformat to 80 columns [#207]
shinyford Aug 18, 2025
6be791c
fix: line endings on generated content
slightfoot Aug 18, 2025
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
2 changes: 1 addition & 1 deletion .fvmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"flutter": "3.10.0"
"flutter": "3.27.4"
}
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Install flutter
uses: flutter-actions/setup-flutter@v4
with:
version: '3.10.0'
version: '3.27.4'

- name: 'Install tools: melos and fvm'
run: dart pub global activate melos && dart pub global activate fvm
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk_auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ for your users to sign up, sign in, and manage their profile from your Dart code

## Requirements

* Dart >= 3.0.0
* Dart >= 3.6.2

## Example Usage

Expand Down
99 changes: 50 additions & 49 deletions packages/clerk_auth/lib/src/clerk_api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import 'package:http/http.dart' as http;

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

typedef _JsonObject = Map<String, dynamic>;

/// [Api] manages communication with the Clerk frontend API
///
class Api with Logging {
Expand Down Expand Up @@ -50,6 +52,7 @@ class Api with Logging {
static const _kClerkSessionId = '_clerk_session_id';
static const _kClientKey = 'client';
static const _kErrorsKey = 'errors';
static const _kMetaKey = 'meta';
static const _kIsNative = '_is_native';
static const _kJwtKey = 'jwt';
static const _kOrganizationId = 'organization_id';
Expand Down Expand Up @@ -92,7 +95,7 @@ class Api with Logging {
Future<Environment> environment() async {
final resp = await _fetch(path: '/environment', method: HttpMethod.get);
if (resp.statusCode == HttpStatus.ok) {
final body = json.decode(resp.body) as Map<String, dynamic>;
final body = json.decode(resp.body) as _JsonObject;
final env = Environment.fromJson(body);

_testMode = env.config.testMode && config.isTestMode;
Expand All @@ -110,7 +113,7 @@ class Api with Logging {
headers: _headers(method: method),
);
if (resp.statusCode == HttpStatus.ok) {
final body = json.decode(resp.body) as Map<String, dynamic>;
final body = json.decode(resp.body) as _JsonObject;
return Client.fromJson(body[_kResponseKey]);
}
return Client.empty;
Expand Down Expand Up @@ -212,8 +215,8 @@ class Api with Logging {
'code': code,
'token': token,
'legal_accepted': legalAccepted,
if (metadata is Map) //
'unsafe_metadata': json.encode(metadata!),
if (metadata case Map<String, dynamic> metadata) //
'unsafe_metadata': json.encode(metadata),
},
);
}
Expand Down Expand Up @@ -250,8 +253,8 @@ class Api with Logging {
'code': code,
'token': token,
'legal_accepted': legalAccepted,
if (metadata is Map) //
'unsafe_metadata': json.encode(metadata!),
if (metadata case Map<String, dynamic> metadata) //
'unsafe_metadata': json.encode(metadata),
},
);
}
Expand Down Expand Up @@ -308,6 +311,8 @@ class Api with Logging {
Strategy? strategy,
String? identifier,
String? password,
String? token,
String? code,
String? redirectUrl,
}) async {
return await _fetchApiResponse(
Expand All @@ -316,6 +321,8 @@ class Api with Logging {
'strategy': strategy,
'identifier': identifier,
'password': password,
'token': token,
'code': code,
'redirect_url': redirectUrl,
},
);
Expand Down Expand Up @@ -445,23 +452,6 @@ class Api with Logging {
);
}

/// Send a token and code supplied by an oAuth provider to the back end
///
Future<ApiResponse> oauthTokenSignIn(
Strategy strategy, {
String? token,
String? code,
}) async {
return await _fetchApiResponse(
'/client/sign_ins',
params: {
'strategy': strategy,
'token': token,
'code': code,
},
);
}

/// Send a token received from an oAuth provider to the back end
///
Future<ApiResponse> sendOauthToken(
Expand Down Expand Up @@ -824,7 +814,7 @@ class Api with Logging {
nullableKeys: [_kOrganizationId],
);
if (resp.statusCode == HttpStatus.ok) {
final body = json.decode(resp.body) as Map<String, dynamic>;
final body = json.decode(resp.body) as _JsonObject;
final token = body[_kJwtKey] as String;
final sessionToken =
_tokenCache.makeAndCacheSessionToken(token, templateName);
Expand Down Expand Up @@ -873,7 +863,7 @@ class Api with Logging {
String url, {
HttpMethod method = HttpMethod.post,
Map<String, String>? headers,
Map<String, dynamic>? params,
_JsonObject? params,
bool withSession = false,
}) async {
try {
Expand Down Expand Up @@ -903,45 +893,56 @@ class Api with Logging {
}

ApiResponse _processResponse(http.Response resp) {
final body = json.decode(resp.body) as Map<String, dynamic>;
final errors = body[_kErrorsKey] != null
? List<Map<String, dynamic>>.from(body[_kErrorsKey])
.map(ApiError.fromJson)
.toList()
: null;
final [clientData, responseData] = switch (body[_kClientKey]) {
Map<String, dynamic> client when client.isNotEmpty => [
client,
body[_kResponseKey],
],
_ => [body[_kResponseKey], null],
};
if (clientData case Map<String, dynamic> clientJson) {
final client = Client.fromJson(clientJson);
final body = json.decode(resp.body) as _JsonObject;
final errors = _extractErrors(body[_kErrorsKey]);
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,
response: switch (responseData) {
Map<String, dynamic> response => response,
_ => null,
},
response: responseData,
);
} else {
logSevere(body);
return ApiResponse(
status: resp.statusCode,
errors: errors,
);
}
}

(_JsonObject?, _JsonObject?) _extractClientAndResponse(_JsonObject body) {
final response = switch (body[_kResponseKey]) {
_JsonObject response when response.isNotEmpty => response,
_ => null,
};

switch (body[_kClientKey] ?? body[_kMetaKey]?[_kClientKey]) {
case _JsonObject client when client.isNotEmpty:
return (client, response);
default:
return (response, null);
}
}

List<ApiError>? _extractErrors(List<dynamic>? data) {
if (data == null) {
return null;
}

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

Future<http.Response> _fetch({
required String path,
HttpMethod method = HttpMethod.post,
Map<String, String>? headers,
Map<String, dynamic>? params,
_JsonObject? params,
bool withSession = false,
List<String>? nullableKeys,
}) async {
Expand Down Expand Up @@ -975,10 +976,10 @@ class Api with Logging {
return resp;
}

Map<String, dynamic> _queryParams(
_JsonObject _queryParams(
HttpMethod method, {
bool withSession = false,
Map<String, dynamic>? params,
_JsonObject? params,
}) {
final sessionId =
params?.remove(_kClerkSessionId)?.toString() ?? _tokenCache.sessionId;
Expand All @@ -992,7 +993,7 @@ class Api with Logging {
};
}

Uri _uri(String path, {Map<String, dynamic>? params}) {
Uri _uri(String path, {_JsonObject? params}) {
return Uri(
scheme: _scheme,
host: _domain,
Expand Down
21 changes: 8 additions & 13 deletions packages/clerk_auth/lib/src/clerk_auth/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -353,22 +353,17 @@ class Auth {
String? token,
String? redirectUrl,
}) async {
// oAuthToken
if (strategy.isOauthToken && (token is String || code is String)) {
await _api
.oauthTokenSignIn(strategy, token: token, code: code)
.then(_housekeeping);
return;
}

// google one tap
if (strategy == Strategy.googleOneTap && token is String) {
await _api
.oauthTokenSignIn(Strategy.googleOneTap, token: token)
.then(_housekeeping);
if (strategy.isOauthToken) {
if (token?.isNotEmpty == true || code?.isNotEmpty == true) {
await _api
.createSignIn(strategy: strategy, token: token, code: code)
.then(_housekeeping);
update();
}
return;
}

// Ensure we have a signIn object
if (client.signIn == null) {
// if password and identifier been presented, we can immediately attempt
// a sign in; if null they will be ignored
Expand Down
20 changes: 10 additions & 10 deletions packages/clerk_auth/lib/src/models/client/strategy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Strategy {
/// provider
final String? provider;

static const _oauthTokenGoogleName = 'google_one_tap';
static const _oauthToken = 'oauth_token';
static const _oauthCustom = 'oauth_custom';
static const _oauth = 'oauth';
Expand All @@ -49,12 +50,8 @@ class Strategy {
/// oauth token apple strategy
static const oauthTokenApple = Strategy(name: _oauthToken, provider: 'apple');

/// oauth token google strategy
static const oauthTokenGoogle =
Strategy(name: _oauthToken, provider: 'google');

/// google one tap strategy
static const googleOneTap = Strategy(name: 'google_one_tap');
/// google authentication token strategy (google one tap)
static const oauthTokenGoogle = Strategy(name: _oauthTokenGoogleName);

/// the collected oauth strategies
static final oauthStrategies = {
Expand All @@ -64,7 +61,6 @@ class Strategy {
oauthFacebook.toString(): oauthFacebook,
oauthTokenApple.toString(): oauthTokenApple,
oauthTokenGoogle.toString(): oauthTokenGoogle,
googleOneTap.toString(): googleOneTap,
};

// verification strategies
Expand Down Expand Up @@ -155,11 +151,15 @@ class Strategy {
/// is known?
bool get isKnown => isUnknown == false;

/// is oauth?
bool get isOauth => const [_oauthToken, _oauthCustom, _oauth].contains(name);
/// is some variety of oauth?
bool get isOauth => name == _oauth || isOauthCustom || isOauthToken;

/// is oauth custom?
bool get isOauthCustom => name == _oauthCustom;

/// is oauth token?
bool get isOauthToken => name == _oauthToken;
bool get isOauthToken =>
const [_oauthToken, _oauthTokenGoogleName].contains(name);

/// is other strategy?
bool get isOtherStrategy => isOauth == false && requiresPassword == false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ class Environment with InformativeToStringMixin {
List<Strategy> get strategies => config.identificationStrategies;

/// [Iterable] of non-oauth and non-phone identification strategies
Iterable<Strategy> get identificationStrategies =>
strategies.where((i) => i.isOauth == false);
Iterable<Strategy> get identificationStrategies => strategies.where(
(i) => const [
Strategy.emailAddress,
Strategy.username,
Strategy.phoneNumber
].contains(i),
);

/// Do we have identification strategies?
bool get hasIdentificationStrategies => identificationStrategies.isNotEmpty;
Expand Down
8 changes: 6 additions & 2 deletions packages/clerk_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ for your users to sign up, sign in, and manage their profile from your Flutter c

## Requirements

* Flutter >= 3.10.0
* Dart >= 3.0.0
* Flutter >= 3.27.4
* Dart >= 3.6.2

## In Development

Expand All @@ -30,6 +30,10 @@ for your users to sign up, sign in, and manage their profile from your Flutter c
To use this package you will need to go to your [Clerk Dashboard](https://dashboard.clerk.com/)
create an application and copy the public and publishable API keys into your project.

The bundled example app requires one, possibly two, variables to be set up in your environment:
- `publishable_key`: your Clerk publishable key, usually starting `pk_`
- `google_client_id`: the ID of your GCP web project, if you are using Google token oauth

```dart
/// Example App
class ExampleApp extends StatelessWidget {
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk_flutter/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
Loading
Loading