diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml index 1272babc..0ae85b88 100644 --- a/.github/workflows/dart_code_metrics.yaml +++ b/.github/workflows/dart_code_metrics.yaml @@ -33,7 +33,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" - run: flutter pub global activate melos 1.0.0-dev.3 + run: flutter pub global activate melos 1.0.0-dev.6 - name: "Bootstrap Workspace" run: melos bootstrap diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 1eb5082a..65913a18 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -31,7 +31,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: | - flutter pub global activate melos 1.0.0-dev.3 + flutter pub global activate melos 1.0.0-dev.6 - name: "Bootstrap Workspace" run: melos bootstrap - name: "Dart Analyze" diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 6b6ae933..c9bdebc1 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -44,10 +44,6 @@ final _levelEmojiMapper = { Level.SEVERE: '🚨', }; -final _userAgent = 'stream-chat-dart-client-' - '${CurrentPlatform.name}-' - '${PACKAGE_VERSION.split('+')[0]}'; - /// The official Dart client for Stream Chat, /// a service for building chat applications. /// This library can be used on any Dart project and on both mobile and web apps @@ -86,7 +82,7 @@ class StreamChatClient { location: location, connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, - headers: {'X-Stream-Client': _userAgent}, + headers: {'X-Stream-Client': defaultUserAgent}, ); _chatApi = chatApi ?? @@ -106,7 +102,7 @@ class StreamChatClient { tokenManager: _tokenManager, handler: handleEvent, logger: detachedLogger('🔌'), - queryParameters: {'X-Stream-Client': _userAgent}, + queryParameters: {'X-Stream-Client': defaultUserAgent}, ); _retryPolicy = retryPolicy ?? @@ -131,6 +127,14 @@ class StreamChatClient { _originalChatPersistenceClient = value; } + /// Default user agent for all requests + static String defaultUserAgent = 'stream-chat-dart-client-' + '${CurrentPlatform.name}-' + '${PACKAGE_VERSION.split('+')[0]}'; + + /// Additionals headers for all requests + static Map additionalHeaders = {}; + ChatPersistenceClient? _originalChatPersistenceClient; /// Chat persistence client diff --git a/packages/stream_chat/lib/src/core/http/interceptor/additional_headers_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/additional_headers_interceptor.dart new file mode 100644 index 00000000..466db58f --- /dev/null +++ b/packages/stream_chat/lib/src/core/http/interceptor/additional_headers_interceptor.dart @@ -0,0 +1,17 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/stream_chat.dart'; + +/// Interceptor that sets additional headers for all requests. +class AdditionalHeadersInterceptor extends Interceptor { + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + options.headers = { + ...options.headers, + ...StreamChatClient.additionalHeaders, + }; + return handler.next(options); + } +} diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index c19cdb8a..429eb52f 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; @@ -41,6 +42,7 @@ class StreamHttpClient { ..._options.headers, } ..interceptors.addAll([ + AdditionalHeadersInterceptor(), if (tokenManager != null) AuthInterceptor(this, tokenManager), if (connectionIdManager != null) ConnectionIdInterceptor(connectionIdManager), diff --git a/packages/stream_chat/test/src/core/http/interceptor/additional_headers_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/additional_headers_interceptor_test.dart new file mode 100644 index 00000000..88cf42ae --- /dev/null +++ b/packages/stream_chat/test/src/core/http/interceptor/additional_headers_interceptor_test.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; +import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +void main() { + late AdditionalHeadersInterceptor additionalHeadersInterceptor; + + setUp(() { + additionalHeadersInterceptor = AdditionalHeadersInterceptor(); + }); + + test( + '`onRequest` should add additional headers in the request', + () async { + final options = RequestOptions(path: 'test-path'); + final handler = RequestInterceptorHandler(); + + StreamChatClient.additionalHeaders = {'test-header': 'test-value'}; + additionalHeadersInterceptor.onRequest(options, handler); + + final updatedOptions = (await handler.future).data as RequestOptions; + final updateHeaders = updatedOptions.headers; + + expect(updateHeaders.containsKey('test-header'), isTrue); + expect(updateHeaders['test-header'], 'test-value'); + }, + ); +} diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index 7860a0d6..a42a5324 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/connection_id_manager.dart'; +import 'package:stream_chat/src/core/http/interceptor/additional_headers_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart'; import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; @@ -48,12 +49,23 @@ void main() { return dioError; } + test('UserAgentInterceptor should be added', () { + const apiKey = 'api-key'; + final client = StreamHttpClient(apiKey); + + expect( + client.httpClient.interceptors + .whereType() + .length, + 1); + }); + test('AuthInterceptor should be added if tokenManager is provided', () { const apiKey = 'api-key'; final client = StreamHttpClient(apiKey, tokenManager: TokenManager()); - expect(client.httpClient.interceptors.length, 1); - expect(client.httpClient.interceptors.first, isA()); + expect( + client.httpClient.interceptors.whereType().length, 1); }); test( @@ -65,10 +77,11 @@ void main() { connectionIdManager: ConnectionIdManager(), ); - expect(client.httpClient.interceptors.length, 1); expect( - client.httpClient.interceptors.first, - isA(), + client.httpClient.interceptors + .whereType() + .length, + 1, ); }, ); @@ -80,10 +93,9 @@ void main() { logger: Logger('test-logger'), ); - expect(client.httpClient.interceptors.length, 1); expect( - client.httpClient.interceptors.first, - isA(), + client.httpClient.interceptors.whereType().length, + 1, ); }); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index ba8bcf64..327a0927 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -110,7 +110,15 @@ class StreamChatState extends State { onBackgroundEventReceived: widget.onBackgroundEventReceived, backgroundKeepAlive: widget.backgroundKeepAlive, connectivityStream: widget.connectivityStream, - child: widget.child ?? const Offstage(), + child: Builder( + builder: (context) { + StreamChatClient.additionalHeaders = { + 'X-Stream-Client': + '${StreamChatClient.defaultUserAgent}-ui', + }; + return widget.child ?? const Offstage(); + }, + ), ), ); }, diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 31410e63..407da315 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -120,7 +120,7 @@ class HomeScreen extends StatelessWidget { itemBuilder: (BuildContext context, int index) { final _item = channels[index]; return ListTile( - title: Text(_item.name!), + title: Text(_item.name ?? ''), subtitle: StreamBuilder( stream: _item.state!.lastMessageStream, initialData: _item.state!.lastMessage, diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 3d07b3c7..3d00b739 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -95,7 +95,12 @@ class StreamChatCoreState extends State Timer? _disconnectTimer; @override - Widget build(BuildContext context) => widget.child; + Widget build(BuildContext context) { + StreamChatClient.additionalHeaders = { + 'X-Stream-Client': '${StreamChatClient.defaultUserAgent}-core', + }; + return widget.child; + } // coverage:ignore-start