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/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index c2ea57b5..c217431a 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -118,9 +118,9 @@ void main() { setUpAll(() { // Fallback values - registerFallbackValue(FakeMessage()); - registerFallbackValue>([]); - registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeMessage()); + registerFallbackValue([]); + registerFallbackValue(FakeAttachmentFile()); // detached loggers when(() => client.detachedLogger(any())).thenAnswer((invocation) { @@ -176,9 +176,9 @@ void main() { setUpAll(() { // Fallback values - registerFallbackValue(FakeMessage()); - registerFallbackValue(FakeAttachmentFile()); - registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(FakeAttachmentFile()); + registerFallbackValue(FakeEvent()); // detached loggers when(() => client.detachedLogger(any())).thenAnswer((invocation) { diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index b59faa38..36c5919e 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -31,7 +31,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -230,7 +230,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -311,7 +311,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -399,7 +399,7 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeUser()); + registerFallbackValue(FakeUser()); }); setUp(() { @@ -523,9 +523,9 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeEvent()); - registerFallbackValue(const PaginationParams()); - registerFallbackValue(FakeChannelState()); + registerFallbackValue(FakeEvent()); + registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeChannelState()); }); setUp(() async { @@ -827,9 +827,9 @@ void main() { setUpAll(() { // fallback values - registerFallbackValue(FakeEvent()); - registerFallbackValue(FakeMessage()); - registerFallbackValue(const PaginationParams()); + registerFallbackValue(FakeEvent()); + registerFallbackValue(FakeMessage()); + registerFallbackValue(const PaginationParams()); }); setUp(() async { diff --git a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart index 27f4ce6a..58ffdefb 100644 --- a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart +++ b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart @@ -15,7 +15,7 @@ void main() { setUp(() { fileUploader = StreamAttachmentFileUploader(client); - registerFallbackValue(FakeMultiPartFile()); + registerFallbackValue(FakeMultiPartFile()); }); Response successResponse(String path, {Object? data}) => Response( 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/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart index b88cef14..d32f3969 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart index 3bc335ce..b0c487ea 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart index 7ec70ab0..5c4f22f1 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:math'; import 'dart:ui'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart index 640eafa5..0cc091c9 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart index 20858e75..55574003 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; 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 diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index a237a23c..e6365868 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -10,7 +10,7 @@ import 'mocks.dart'; void main() { setUpAll(() { - registerFallbackValue(const PaginationParams()); + registerFallbackValue(const PaginationParams()); }); List _generateChannels(