diff --git a/example/pubspec.lock b/example/pubspec.lock index 5dc0f27..f8b4d46 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,23 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + app_links: + dependency: transitive + description: + name: app_links + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" args: dependency: transitive description: @@ -34,50 +61,50 @@ packages: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -130,10 +157,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: @@ -249,10 +276,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: @@ -313,18 +348,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -356,22 +391,14 @@ packages: relative: true source: path version: "3.0.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -384,10 +411,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -424,10 +451,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: transitive description: @@ -552,10 +579,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -581,66 +608,66 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: transitive description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" typed_data: dependency: transitive description: @@ -725,10 +752,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -757,10 +784,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -773,10 +800,10 @@ packages: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.10.1" window_to_front: dependency: transitive description: @@ -810,5 +837,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/logto_client.dart b/lib/logto_client.dart index 8e66ebd..dc14665 100644 --- a/lib/logto_client.dart +++ b/lib/logto_client.dart @@ -370,4 +370,4 @@ class LogtoClient { if (_httpClient == null) httpClient.close(); } } -} +} \ No newline at end of file diff --git a/lib/logto_client_custom.dart b/lib/logto_client_custom.dart new file mode 100644 index 0000000..47d8125 --- /dev/null +++ b/lib/logto_client_custom.dart @@ -0,0 +1,427 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:app_links/app_links.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:http/http.dart' as http; +import 'package:jose/jose.dart'; + +import '/src/utilities/stream_awaiter.dart'; +import '/src/modules/callback_strategy.dart'; +import '/src/exceptions/logto_auth_exceptions.dart'; +import '/src/interfaces/logto_interfaces.dart'; +import '/src/modules/id_token.dart'; +import '/src/modules/logto_storage_strategy.dart'; +import '/src/modules/pkce.dart'; +import '/src/modules/token_storage.dart'; +import '/src/utilities/utils.dart' as utils; +import 'logto_core.dart' as logto_core; +import '/src/utilities/constants.dart'; + +export '/src/exceptions/logto_auth_exceptions.dart'; +export '/src/interfaces/logto_interfaces.dart'; +export '/src/utilities/constants.dart'; +export '/src/modules/callback_strategy.dart'; + +/** + * LogtoClient + * + * The main class for the Logto SDK. + * It provides all the user authentication and authorization methods. + * + * @param config: LogtoConfig - the basic configuration object for the Logto SDK. + * @param storageProvider: LogtoStorageStrategy (optional) - default is [InMemoryTokenStorage] used for storing tokens. + * @param httpClient: http.Client (optional) - custom [http.Client] to be used for making http requests. + * + * Example: + * ```dart + * final config = LogtoConfig( + * appId: 'oOeT50aNvY7QbLci6XJZt', + * endpoint: 'http://localhost:3001/', + * ); + * + * final logtoClient = LogtoClient(config); + */ +final appLinks = AppLinks(); + +class LogtoClientCustom { + final LogtoConfig config; + + late PKCE _pkce; + late String _state; + + static late TokenStorage _tokenStorage; + + // Custom [http.Client]. + // Note that you will have to call `close()` yourself when passing a [http.Client] instance. + late final http.Client? _httpClient; + + bool _loading = false; + + bool get loading => _loading; + + OidcProviderConfig? _oidcConfig; + + late final CallbackStrategy _callbackStrategy; + + LogtoClientCustom( + {required this.config, + LogtoStorageStrategy? storageProvider, + http.Client? httpClient, + CallbackStrategy? callbackStrategy}) { + _httpClient = httpClient; + _tokenStorage = TokenStorage(storageProvider); + _callbackStrategy = callbackStrategy ?? SchemeStrategy(); + } + + final _authController = StreamController.broadcast(); + + Stream get isAuthenticatedStream => _authController.stream; + + void init() async { + bool value = await isAuthenticated; + + _authController.sink.add(value); + } + + // Use idToken to check if the user is authenticated. + Future get isAuthenticated async { + return await _tokenStorage.idToken != null; + } + + Future get idToken async { + final token = await _tokenStorage.idToken; + return token?.serialization; + } + + Future get idTokenClaims async { + final token = await _tokenStorage.idToken; + return token?.claims; + } + + Future _getOidcConfig(http.Client httpClient) async { + if (_oidcConfig != null) { + return _oidcConfig!; + } + + final discoveryUri = + utils.appendUriPath(config.endpoint, logto_core.discoveryPath); + _oidcConfig = await logto_core.fetchOidcConfig(httpClient, discoveryUri); + + return _oidcConfig!; + } + + // Get the access token by resource indicator or organizationId. + Future getAccessToken( + {String? resource, String? organizationId}) async { + final accessToken = await _tokenStorage.getAccessToken( + resource: resource, organizationId: organizationId); + + if (accessToken != null) { + return accessToken; + } + + final token = await _getAccessTokenByRefreshToken( + resource: resource, organizationId: organizationId); + + return token; + } + + // Get the access token for the organization by organizationId. + Future getOrganizationToken(String organizationId) async { + if (config.scopes == null || + !config.scopes! + .contains(logto_core.LogtoUserScope.organizations.value)) { + throw LogtoAuthException(LogtoAuthExceptions.missingScopeError, + 'organizations scope is not specified'); + } + + return await getAccessToken(organizationId: organizationId); + } + + // Fetch the access token by refresh token. + // No need to specify the scopes for the resource, all the related scopes in the refresh token's grant list will be returned. + Future _getAccessTokenByRefreshToken( + {String? resource, String? organizationId}) async { + final refreshToken = await _tokenStorage.refreshToken; + + if (refreshToken == null) { + throw LogtoAuthException( + LogtoAuthExceptions.authenticationError, 'not_authenticated'); + } + + final httpClient = _httpClient ?? http.Client(); + + try { + final oidcConfig = await _getOidcConfig(httpClient); + + final response = await logto_core.fetchTokenByRefreshToken( + httpClient: httpClient, + tokenEndPoint: oidcConfig.tokenEndpoint, + clientId: config.appId, + refreshToken: refreshToken, + resource: resource, + organizationId: organizationId); + + final scopes = response.scope.split(' '); + + await _tokenStorage.setAccessToken(response.accessToken, + expiresIn: response.expiresIn, + resource: resource, + organizationId: organizationId, + scopes: scopes); + + // renew refresh token + if (response.refreshToken != null) { + await _tokenStorage.setRefreshToken(response.refreshToken); + } + + // verify and store id_token if not null + if (response.idToken != null) { + final idToken = IdToken.unverified(response.idToken!); + await _verifyIdToken(idToken, oidcConfig); + await _tokenStorage.setIdToken(idToken); + + _authController.sink.add(true); + } + + return await _tokenStorage.getAccessToken( + resource: resource, organizationId: organizationId); + } finally { + if (_httpClient == null) httpClient.close(); + } + } + + Future _verifyIdToken( + IdToken idToken, OidcProviderConfig oidcConfig) async { + final keyStore = JsonWebKeyStore() + ..addKeySetUrl(Uri.parse(oidcConfig.jwksUri)); + + if (!await idToken.verify(keyStore)) { + throw LogtoAuthException( + LogtoAuthExceptions.idTokenValidationError, 'invalid jws signature'); + } + + final violations = idToken.claims + .validate(issuer: Uri.parse(oidcConfig.issuer), clientId: config.appId); + + if (violations.isNotEmpty) { + throw LogtoAuthException( + LogtoAuthExceptions.idTokenValidationError, '$violations'); + } + } + + // Clear the access token by resource indicator or organizationId. + Future clearAccessToken({String? resource, String? organizationId}) { + return _tokenStorage.deleteAccessToken( + resource: resource, organizationId: organizationId); + } + + // Sign in using the PKCE flow. + Future signIn( + String redirectUri, { + logto_core.InteractionMode? interactionMode, + String? loginHint, + String? directSignIn, + FirstScreen? firstScreen, + List? identifiers, + Map? extraParams, + }) async { + if (_loading) { + throw LogtoAuthException( + LogtoAuthExceptions.isLoadingError, 'Already signing in...'); + } + + final httpClient = _httpClient ?? http.Client(); + + try { + _loading = true; + _pkce = PKCE.generate(); + _state = utils.generateRandomString(); + _tokenStorage.setIdToken(null); + final oidcConfig = await _getOidcConfig(httpClient); + + final signInUri = logto_core.generateSignInUri( + authorizationEndpoint: oidcConfig.authorizationEndpoint, + clientId: config.appId, + redirectUri: redirectUri, + codeChallenge: _pkce.codeChallenge, + state: _state, + resources: config.resources, + scopes: config.scopes, + interactionMode: interactionMode, + loginHint: loginHint, + firstScreen: firstScreen, + directSignIn: directSignIn, + identifiers: identifiers, + extraParams: extraParams, + ); + + final String callbackUri = await _getCallbackUrl(signInUri); + + await _handleSignInCallback(callbackUri, redirectUri, httpClient); + } finally { + _loading = false; + if (_httpClient == null) httpClient.close(); + } + } + + // Handle the sign-in callback and complete the token exchange process. + Future _handleSignInCallback( + String callbackUri, String redirectUri, http.Client httpClient) async { + final code = logto_core.verifyAndParseCodeFromCallbackUri( + callbackUri, + redirectUri, + _state, + ); + + final oidcConfig = await _getOidcConfig(httpClient); + + final tokenResponse = await logto_core.fetchTokenByAuthorizationCode( + httpClient: httpClient, + tokenEndPoint: oidcConfig.tokenEndpoint, + code: code, + codeVerifier: _pkce.codeVerifier, + clientId: config.appId, + redirectUri: redirectUri, + ); + + final idToken = IdToken.unverified(tokenResponse.idToken); + + await _verifyIdToken(idToken, oidcConfig); + + await _tokenStorage.save( + idToken: idToken, + accessToken: tokenResponse.accessToken, + refreshToken: tokenResponse.refreshToken, + expiresIn: tokenResponse.expiresIn, + scopes: tokenResponse.scope.split(' ')); + + _authController.sink.add(true); + } + + // Sign out the user. + Future signOut() async { + // Throw error is authentication status not found + final idToken = await _tokenStorage.idToken; + + final httpClient = _httpClient ?? http.Client(); + + if (idToken == null) { + throw LogtoAuthException( + LogtoAuthExceptions.authenticationError, 'not authenticated'); + } + + try { + final oidcConfig = await _getOidcConfig(httpClient); + + // Revoke refresh token if exist + final refreshToken = await _tokenStorage.refreshToken; + + if (refreshToken != null) { + try { + await logto_core.revoke( + httpClient: httpClient, + revocationEndpoint: oidcConfig.revocationEndpoint, + clientId: config.appId, + token: refreshToken, + ); + } catch (e) { + // Do Nothing silently revoke the token + } + } + + await _tokenStorage.clear(); + _authController.sink.add(false); + } finally { + if (_httpClient == null) { + httpClient.close(); + } + } + } + + // Fetch user info from the user info endpoint. + Future getUserInfo() async { + final httpClient = _httpClient ?? http.Client(); + + try { + final oidcConfig = await _getOidcConfig(httpClient); + + final accessToken = await getAccessToken(); + + if (accessToken == null) { + throw LogtoAuthException( + LogtoAuthExceptions.authenticationError, 'not authenticated'); + } + + final userInfoResponse = await logto_core.fetchUserInfo( + httpClient: httpClient, + userInfoEndpoint: oidcConfig.userInfoEndpoint, + accessToken: accessToken.token, + ); + + return userInfoResponse; + } finally { + if (_httpClient == null) httpClient.close(); + } + } + + Future _getCallbackUrl(Uri url) async { + final LaunchMode launchMode = + _callbackStrategy.launchMode == BrowserLaunchMode.platformDefault + ? LaunchMode.platformDefault + : LaunchMode.externalApplication; + + if (!await launchUrl(url, mode: launchMode)) { + throw Exception('Could not launch ${url.toString()}'); + } + + var result = await _athuneticateUserFlow(); + + return result.toString(); + } + + Future _athuneticateUserFlow() async { + late Uri result; + + if (_callbackStrategy.strategy == CallbackStrategyType.scheme) { + await awaitUriLinkStream( + appLinks.uriLinkStream, + (uri) async { + if (uri != null) { + result = uri; + } else { + throw Exception("Failed to authorize"); + } + }, + ); + + return result; + } + + final server = await HttpServer.bind(InternetAddress.anyIPv4, + (_callbackStrategy as LocalServerStrategy).port); + + await for (HttpRequest request in server) { + // Handle the request + request.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.html + ..writeln(""" + + + +"""); + await request.response.close(); + result = request.requestedUri; + await server.close(); + break; // Exit the loop + } + + return result; + } + + void dispose() { + _authController.close(); + } +} diff --git a/lib/logto_dart_sdk.dart b/lib/logto_dart_sdk.dart index 42064ec..73894f2 100644 --- a/lib/logto_dart_sdk.dart +++ b/lib/logto_dart_sdk.dart @@ -1,2 +1,5 @@ +library logto_dart_sdk; + +export 'logto_client_custom.dart'; export 'logto_client.dart'; export 'logto_core.dart'; diff --git a/lib/src/modules/callback_strategy.dart b/lib/src/modules/callback_strategy.dart new file mode 100644 index 0000000..d5bd555 --- /dev/null +++ b/lib/src/modules/callback_strategy.dart @@ -0,0 +1,49 @@ +enum CallbackStrategyType { + scheme, + localServer +} + +enum BrowserLaunchMode { + platformDefault, + external +} + +abstract class CallbackStrategy { + CallbackStrategyType get strategy; + BrowserLaunchMode get launchMode; +} + +class SchemeStrategy implements CallbackStrategy { + late BrowserLaunchMode _launchMode; + final CallbackStrategyType _strategy = CallbackStrategyType.scheme; + + SchemeStrategy({BrowserLaunchMode? launchMode}){ + _launchMode = launchMode ?? BrowserLaunchMode.platformDefault; + } + + @override + CallbackStrategyType get strategy => _strategy; + + @override + BrowserLaunchMode get launchMode => _launchMode; + +} + +class LocalServerStrategy implements CallbackStrategy { + late BrowserLaunchMode _launchMode; + final CallbackStrategyType _strategy = CallbackStrategyType.localServer; + final int _port; + + LocalServerStrategy(this._port,{BrowserLaunchMode? launchMode}){ + _launchMode = launchMode ?? BrowserLaunchMode.platformDefault; + } + + @override + CallbackStrategyType get strategy => _strategy; + + int get port => _port; + + @override + BrowserLaunchMode get launchMode => _launchMode; + +} \ No newline at end of file diff --git a/lib/src/utilities/stream_awaiter.dart b/lib/src/utilities/stream_awaiter.dart new file mode 100644 index 0000000..3ef701c --- /dev/null +++ b/lib/src/utilities/stream_awaiter.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +Future awaitUriLinkStream(Stream uriLinkStream, Future Function(Uri? uri) onUri) async { + final completer = Completer(); + late StreamSubscription subscription; + + subscription = uriLinkStream.listen( + (uri) async { + try { + await onUri(uri); + completer.complete(); + } catch (e) { + completer.completeError(e); + } finally { + await subscription.cancel(); // Ensure subscription is canceled + } + }, + onError: (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(); + } + }, + cancelOnError: true, + ); + + return completer.future; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 1c1262e..14cf602 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,9 @@ dependencies: jose: ^0.3.4 json_annotation: ^4.9.0 path: ^1.8.1 + collection: ^1.17.1 + url_launcher: ^6.3.1 + app_links: ^6.3.2 flutter_web_auth_2: ^4.1.0 dev_dependencies: