From 85c6c4eb429c0cf752f2f82f2dc6fceb19b7c449 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:19:40 +0200 Subject: [PATCH 1/9] Add dart client --- dart/.gitattributes | 2 + dart/.gitignore | 41 + dart/README.md | 12 + dart/analysis_options.yaml | 5 + dart/lib/leancode_pipe.dart | 4 + .../leancode_pipe/contracts/contracts.dart | 94 +++ .../leancode_pipe/contracts/contracts.g.dart | 70 ++ dart/lib/leancode_pipe/contracts/topic.dart | 9 + .../leancode_pipe/create_hub_connection.dart | 22 + dart/lib/leancode_pipe/pipe_client.dart | 607 +++++++++++++++ .../leancode_pipe/pipe_connection_state.dart | 17 + .../pipe_connection_state_mapper.dart | 16 + dart/lib/leancode_pipe/pipe_error.dart | 15 + dart/lib/leancode_pipe/pipe_subscription.dart | 20 + .../lib/leancode_pipe/topic_subscription.dart | 107 +++ dart/pubspec.lock | 607 +++++++++++++++ dart/pubspec.yaml | 30 + dart/test/leancode_pipe/common/mocks.dart | 33 + dart/test/leancode_pipe/common/utils.dart | 186 +++++ dart/test/leancode_pipe/common/wait.dart | 7 + .../lean_pipe_client_connection_test.dart | 172 +++++ .../leancode_pipe/lean_pipe_client_test.dart | 711 ++++++++++++++++++ .../topic_subscription_test.dart | 93 +++ 23 files changed, 2880 insertions(+) create mode 100644 dart/.gitattributes create mode 100644 dart/.gitignore create mode 100644 dart/README.md create mode 100644 dart/analysis_options.yaml create mode 100644 dart/lib/leancode_pipe.dart create mode 100644 dart/lib/leancode_pipe/contracts/contracts.dart create mode 100644 dart/lib/leancode_pipe/contracts/contracts.g.dart create mode 100644 dart/lib/leancode_pipe/contracts/topic.dart create mode 100644 dart/lib/leancode_pipe/create_hub_connection.dart create mode 100644 dart/lib/leancode_pipe/pipe_client.dart create mode 100644 dart/lib/leancode_pipe/pipe_connection_state.dart create mode 100644 dart/lib/leancode_pipe/pipe_connection_state_mapper.dart create mode 100644 dart/lib/leancode_pipe/pipe_error.dart create mode 100644 dart/lib/leancode_pipe/pipe_subscription.dart create mode 100644 dart/lib/leancode_pipe/topic_subscription.dart create mode 100644 dart/pubspec.lock create mode 100644 dart/pubspec.yaml create mode 100644 dart/test/leancode_pipe/common/mocks.dart create mode 100644 dart/test/leancode_pipe/common/utils.dart create mode 100644 dart/test/leancode_pipe/common/wait.dart create mode 100644 dart/test/leancode_pipe/lean_pipe_client_connection_test.dart create mode 100644 dart/test/leancode_pipe/lean_pipe_client_test.dart create mode 100644 dart/test/leancode_pipe/topic_subscription_test.dart diff --git a/dart/.gitattributes b/dart/.gitattributes new file mode 100644 index 0000000..243a769 --- /dev/null +++ b/dart/.gitattributes @@ -0,0 +1,2 @@ +*.freezed.dart linguist-generated=true +*.g.dart linguist-generated=true diff --git a/dart/.gitignore b/dart/.gitignore new file mode 100644 index 0000000..0d757ae --- /dev/null +++ b/dart/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# iOS ipa file and symbols +*.ipa +*.dSYM.zip + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/dart/README.md b/dart/README.md new file mode 100644 index 0000000..7b82329 --- /dev/null +++ b/dart/README.md @@ -0,0 +1,12 @@ +# leancode_pipe + +## Getting Started + +# Android + +Add these into your AndroidManifest.xml on the manifest level + +```xml + + +``` diff --git a/dart/analysis_options.yaml b/dart/analysis_options.yaml new file mode 100644 index 0000000..6343f69 --- /dev/null +++ b/dart/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:leancode_lint/analysis_options.yaml + +analyzer: + exclude: + - "**/*.g.dart" diff --git a/dart/lib/leancode_pipe.dart b/dart/lib/leancode_pipe.dart new file mode 100644 index 0000000..8c39da5 --- /dev/null +++ b/dart/lib/leancode_pipe.dart @@ -0,0 +1,4 @@ +export 'leancode_pipe/contracts/topic.dart'; +export 'leancode_pipe/pipe_client.dart'; +export 'leancode_pipe/pipe_connection_state.dart'; +export 'leancode_pipe/pipe_subscription.dart'; diff --git a/dart/lib/leancode_pipe/contracts/contracts.dart b/dart/lib/leancode_pipe/contracts/contracts.dart new file mode 100644 index 0000000..3207d3d --- /dev/null +++ b/dart/lib/leancode_pipe/contracts/contracts.dart @@ -0,0 +1,94 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'contracts.g.dart'; + +const _pipeContractsSerializable = JsonSerializable( + fieldRename: FieldRename.pascal, +); + +@_pipeContractsSerializable +class SubscriptionResult { + const SubscriptionResult({ + required this.subscriptionId, + required this.status, + required this.type, + }); + + factory SubscriptionResult.fromJson(Map json) => + _$SubscriptionResultFromJson(json); + + final String subscriptionId; + final SubscriptionStatus status; + final OperationType type; + + Map toJson() => _$SubscriptionResultToJson(this); +} + +enum SubscriptionStatus { + @JsonValue(0) + success, + @JsonValue(1) + unauthorized, + @JsonValue(2) + malformed, + @JsonValue(3) + invalid, + @JsonValue(4) + internalServerError, +} + +enum OperationType { + @JsonValue(0) + subscribe, + @JsonValue(1) + unsubscribe, +} + +@_pipeContractsSerializable +class NotificationEnvelope { + const NotificationEnvelope({ + required this.id, + required this.topicType, + required this.notificationType, + required this.topic, + required this.notification, + }); + + factory NotificationEnvelope.fromJson(Map json) => + _$NotificationEnvelopeFromJson(json); + + final String id; + final String topicType; + final String notificationType; + final Map topic; + final Object notification; + + Map toJson() => _$NotificationEnvelopeToJson(this); + + @override + String toString() { + return 'NotificationEnvelope(id: $id, topicType: $topicType, notificationType: $notificationType, topic: $topic, notification: $notification)'; + } +} + +@_pipeContractsSerializable +class SubscriptionEnvelope { + const SubscriptionEnvelope({ + required this.id, + required this.topic, + required this.topicType, + }); + + factory SubscriptionEnvelope.fromJson(Map json) => + _$SubscriptionEnvelopeFromJson(json); + + final String id; + final String topic; + final String topicType; + + Map toJson() => _$SubscriptionEnvelopeToJson(this); + + @override + String toString() => + 'SubscriptionEnvelope(id: $id, topic: $topic, topicType: $topicType)'; +} diff --git a/dart/lib/leancode_pipe/contracts/contracts.g.dart b/dart/lib/leancode_pipe/contracts/contracts.g.dart new file mode 100644 index 0000000..3462e69 --- /dev/null +++ b/dart/lib/leancode_pipe/contracts/contracts.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contracts.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SubscriptionResult _$SubscriptionResultFromJson(Map json) => + SubscriptionResult( + subscriptionId: json['SubscriptionId'] as String, + status: $enumDecode(_$SubscriptionStatusEnumMap, json['Status']), + type: $enumDecode(_$OperationTypeEnumMap, json['Type']), + ); + +Map _$SubscriptionResultToJson(SubscriptionResult instance) => + { + 'SubscriptionId': instance.subscriptionId, + 'Status': _$SubscriptionStatusEnumMap[instance.status]!, + 'Type': _$OperationTypeEnumMap[instance.type]!, + }; + +const _$SubscriptionStatusEnumMap = { + SubscriptionStatus.success: 0, + SubscriptionStatus.unauthorized: 1, + SubscriptionStatus.malformed: 2, + SubscriptionStatus.invalid: 3, + SubscriptionStatus.internalServerError: 4, +}; + +const _$OperationTypeEnumMap = { + OperationType.subscribe: 0, + OperationType.unsubscribe: 1, +}; + +NotificationEnvelope _$NotificationEnvelopeFromJson( + Map json) => + NotificationEnvelope( + id: json['Id'] as String, + topicType: json['TopicType'] as String, + notificationType: json['NotificationType'] as String, + topic: json['Topic'] as Map, + notification: json['Notification'] as Object, + ); + +Map _$NotificationEnvelopeToJson( + NotificationEnvelope instance) => + { + 'Id': instance.id, + 'TopicType': instance.topicType, + 'NotificationType': instance.notificationType, + 'Topic': instance.topic, + 'Notification': instance.notification, + }; + +SubscriptionEnvelope _$SubscriptionEnvelopeFromJson( + Map json) => + SubscriptionEnvelope( + id: json['Id'] as String, + topic: json['Topic'] as String, + topicType: json['TopicType'] as String, + ); + +Map _$SubscriptionEnvelopeToJson( + SubscriptionEnvelope instance) => + { + 'Id': instance.id, + 'Topic': instance.topic, + 'TopicType': instance.topicType, + }; diff --git a/dart/lib/leancode_pipe/contracts/topic.dart b/dart/lib/leancode_pipe/contracts/topic.dart new file mode 100644 index 0000000..b03b3d5 --- /dev/null +++ b/dart/lib/leancode_pipe/contracts/topic.dart @@ -0,0 +1,9 @@ +abstract interface class Topic { + Notification? castNotification(String fullName, dynamic json); + + String getFullName(); + + Map toJson(); + + Topic fromJson(Map json); +} diff --git a/dart/lib/leancode_pipe/create_hub_connection.dart b/dart/lib/leancode_pipe/create_hub_connection.dart new file mode 100644 index 0000000..1fc33f8 --- /dev/null +++ b/dart/lib/leancode_pipe/create_hub_connection.dart @@ -0,0 +1,22 @@ +import 'package:leancode_pipe/leancode_pipe/pipe_client.dart'; +import 'package:signalr_core/signalr_core.dart'; + +HubConnection createHubConnection({ + required String pipeUrl, + PipeTokenFactory? tokenFactory, +}) { + return HubConnectionBuilder() + .withUrl( + pipeUrl, + HttpConnectionOptions( + transport: HttpTransportType.webSockets, + accessTokenFactory: tokenFactory, + ), + ) + .withAutomaticReconnect( + DefaultReconnectPolicy( + retryDelays: [0, 2000, 5000, 10000, 20000, 30000, null], + ), + ) + .build(); +} diff --git a/dart/lib/leancode_pipe/pipe_client.dart b/dart/lib/leancode_pipe/pipe_client.dart new file mode 100644 index 0000000..db28bc4 --- /dev/null +++ b/dart/lib/leancode_pipe/pipe_client.dart @@ -0,0 +1,607 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:leancode_pipe/leancode_pipe/contracts/contracts.dart'; +import 'package:leancode_pipe/leancode_pipe/contracts/topic.dart'; +import 'package:leancode_pipe/leancode_pipe/create_hub_connection.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_connection_state.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_connection_state_mapper.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_error.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_subscription.dart'; +import 'package:leancode_pipe/leancode_pipe/topic_subscription.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:signalr_core/signalr_core.dart'; +import 'package:uuid/uuid.dart'; + +typedef OnPipeAction = void Function(T value); +typedef PipeTokenFactory = Future Function(); + +class PipeClient { + factory PipeClient({ + required String pipeUrl, + required PipeTokenFactory tokenFactory, + void Function()? onReconnected, + OnPipeAction? onReconnecting, + OnPipeAction? onClose, + }) => + PipeClient.fromMock( + hubConnection: createHubConnection( + pipeUrl: pipeUrl, + tokenFactory: tokenFactory, + ), + onReconnected: onReconnected, + onReconnecting: onReconnecting, + onClose: onClose, + ); + + @visibleForTesting + PipeClient.fromMock({ + required HubConnection hubConnection, + Uuid uuid = const Uuid(), + this.onReconnected, + this.onReconnecting, + this.onClose, + }) : _hubConnection = hubConnection, + _uuid = uuid; + + final HubConnection _hubConnection; + final Uuid _uuid; + + static const _topicSubscriptionReconnectsCount = 3; + static const _signalRRequestTimeoutDuration = Duration(seconds: 30); + + final void Function()? onReconnected; + final OnPipeAction? onReconnecting; + final OnPipeAction? onClose; + + static final _logger = Logger('PipeClient'); + final _registeredTopicSubscriptions = []; + + PipeConnectionState get connectionState => + PipeConnectionStateMapper.fromHubConnectionState( + _hubConnection.state, + ); + + Future connect() async { + if (connectionState != PipeConnectionState.disconnected) { + _logger.warning( + 'Tried to connect to the pipe while connection is already opened.', + ); + return; + } + + try { + _hubConnection + ..onreconnected((connectionId) async { + await Future.wait([ + for (final topicSub in _registeredTopicSubscriptions) + _reconnect(topicSub), + ]); + onReconnected?.call(); + }) + ..onreconnecting((error) { + onReconnecting?.call(error); + }) + ..onclose((error) async { + await Future.wait([ + for (final topicSub in _registeredTopicSubscriptions) + topicSub.close(), + ]); + onClose?.call(error); + }) + ..on('subscriptionResult', _onSubscriptionResult) + ..on('notify', _onNotify); + // FIXME: Verify what does it throw if authorization does not pass through (can be bugged on backend) + await _hubConnection.start(); + } catch (err, st) { + _logger.shout('Could not connect to LeanCode pipe service', err, st); + throw const PipeConnectionException(); + } + } + + Future disconnect() async { + if (connectionState == PipeConnectionState.disconnected) { + _logger.fine( + 'Tried to disconnect while pipe is not connected', + ); + return; + } + + try { + await _hubConnection.stop(); + } catch (err, st) { + _logger.shout('Could not disconnect from LeanCode pipe service', err, st); + rethrow; + } + } + + Future> subscribe( + Topic topic, { + void Function()? onReconnect, + }) async { + final thisTopicSubscription = + _registeredTopicSubscriptions.firstWhereOrNull((e) => e.topic == topic); + + if (thisTopicSubscription != null) { + final state = thisTopicSubscription.stateSubject.value; + switch (state) { + case TopicSubscriptionSubscribed(): + return _registerSubscription( + subscribedState: state, + onReconnect: onReconnect, + topic: topic, + ); + case TopicSubscriptionUnsubscribing(): + // Mark topic subscription to not be fully removed after unsubscribe + state.newSubscriptionRequestsCount++; + final index = state.newSubscriptionRequestsCount; + + // Subscription process will continue after unsubscribing future + await state.completer.future; + + if (index == 1) { + // Prepare for subscribing + final subscribingState = TopicSubscriptionSubscribing(); + thisTopicSubscription.stateSubject.add(subscribingState); + + // Subscribe on backend and return a valid pipe subscription + // (or throw an exception) + return _sendSubscribeMethodAndVerifyResult( + subscribingState: subscribingState, + subscription: thisTopicSubscription, + onReconnect: onReconnect, + subscriptionId: thisTopicSubscription.subscriptionId, + topic: topic, + ); + } else { + // Recurrence should now find the topic subscription in different state + // than unsubscribing (either subscribing or subscribed) + await Future.delayed(const Duration(milliseconds: 100)); + return subscribe(topic, onReconnect: onReconnect); + } + + case TopicSubscriptionSubscribing(): + case TopicSubscriptionReconnecting(): + try { + final state = await thisTopicSubscription.stateSubject + .doOnError((err, st) { + _logger.info( + 'Caught error in second subscription call, while already subscribing/reconnecting.' + 'Will attempt to connect subscribe either way.', + err, + st, + ); + throw const PipeConnectionException(); + }) + .whereType() + .first; + + return _registerSubscription( + subscribedState: state, + onReconnect: onReconnect, + topic: topic, + ); + } catch (_) { + // Continue with subscription process logic below + } + } + } + + // Register subscription in SignalR + final subscriptionId = _uuid.v4(); + + // Register subscription in pipe client + final subscribingState = TopicSubscriptionSubscribing(); + final subscription = + TopicSubscription(topic, subscriptionId, subscribingState); + + _registeredTopicSubscriptions.add(subscription); + + return _sendSubscribeMethodAndVerifyResult( + subscribingState: subscribingState, + subscription: subscription, + onReconnect: onReconnect, + subscriptionId: subscriptionId, + topic: topic, + ); + } + + PipeSubscription _registerSubscription({ + required TopicSubscriptionSubscribed subscribedState, + required Topic topic, + required void Function()? onReconnect, + }) { + final controller = StreamController.broadcast(); + final id = _uuid.v1(); + final subscription = RegisteredSubscription( + id: id, + controller: controller, + unsubscribe: () => _unsubscribe(topic, id), + onReconnect: onReconnect, + ); + + subscribedState.registeredSubscriptions.add(subscription); + + return subscription.pipeSubscription; + } + + Future> + _sendSubscribeMethodAndVerifyResult({ + required Topic topic, + required String subscriptionId, + required TopicSubscriptionSubscribing subscribingState, + required TopicSubscription subscription, + required void Function()? onReconnect, + }) async { + // Register topic subscription in backend service + try { + final result = await _sendPipeServiceMethod( + topic: topic, + subscriptionId: subscriptionId, + completer: subscribingState.completer, + method: _PipeServiceMethod.subscribe, + ); + + switch (result) { + case SubscriptionStatus.success: + // Create [TopicSubscriptionSubscribed] state and add it to states stream of subscription + final subscribedState = TopicSubscriptionSubscribed( + registeredSubscriptions: [], + ); + final pipeSubscription = _registerSubscription( + subscribedState: subscribedState, + topic: topic, + onReconnect: onReconnect, + ); + + subscription.stateSubject.add(subscribedState); + + return pipeSubscription; + case SubscriptionStatus.internalServerError: + case SubscriptionStatus.invalid: + case SubscriptionStatus.malformed: + throw const PipeServerException(); + case SubscriptionStatus.unauthorized: + throw const PipeUnauthorizedException(); + } + } catch (_) { + await subscription.close(); + rethrow; + } + } + + Future _unsubscribe(Topic topic, String id) async { + final thisTopicSubscription = + _registeredTopicSubscriptions.firstWhereOrNull((e) => e.topic == topic); + + if (thisTopicSubscription == null) { + throw StateError( + 'There is no ongoing subscription to requested to unsubscribe topic: $topic', + ); + } + + final state = thisTopicSubscription.stateSubject.value; + + final TopicSubscriptionSubscribed subscribedState; + switch (state) { + case TopicSubscriptionUnsubscribing(): + await state.completer.future; + return; + + case TopicSubscriptionSubscribing(): + throw StateError( + 'Can not unsubscribe from a not registered subscription', + ); + case TopicSubscriptionReconnecting(): + try { + subscribedState = await thisTopicSubscription.stateSubject + .doOnError((err, st) { + // ignore: only_throw_errors + throw err; + }) + .whereType() + .first + .timeout(_signalRRequestTimeoutDuration); + } on PipeReconnectException catch (_) { + // Could not reconnect which means that subscription is not + // tied up with the backend service - in such scenario we can + // asume that unsubscribe finished successfully + _logger.fine( + 'Unsubscribe on topic: $topic completed successfully ' + 'because of a reconnect failure.', + ); + return; + } catch (err, st) { + _logger.shout( + 'Failed to find a valid subscribed state while unsubscribing ' + 'on topic: $topic.', + err, + st, + ); + rethrow; + } + + case TopicSubscriptionSubscribed(): + subscribedState = state; + } + + if (subscribedState.registeredSubscriptions.length > 1) { + final subscription = + subscribedState.registeredSubscriptions.firstWhere((e) => e.id == id); + + subscribedState.registeredSubscriptions.remove(subscription); + + await subscription.controller.close(); + return; + } + + // Add unsubscribing state to pipe client + final unsubscribingState = TopicSubscriptionUnsubscribing( + // Close the notifications stream controller before complete + // to keep async operations together + subscribedState: subscribedState, + ); + thisTopicSubscription.stateSubject.add(unsubscribingState); + + // Unregister subscription in backend service + final result = await _sendPipeServiceMethod( + topic: topic, + subscriptionId: thisTopicSubscription.subscriptionId, + completer: unsubscribingState.completer, + method: _PipeServiceMethod.unsubscribe, + ); + + switch (result) { + case SubscriptionStatus.success: + // If another subscription is waiting for unsubscribe to persume with + // subscribe logic then clear registered subscriptions but leave the + // topic subscription in [_registeredTopicSubscriptions] + if (unsubscribingState.newSubscriptionRequestsCount > 0) { + subscribedState.registeredSubscriptions.clear(); + return; + } + + // Close streams tied up with this subscription + await Future.wait([ + thisTopicSubscription.stateSubject.close(), + for (final subscription in subscribedState.registeredSubscriptions) + subscription.controller.close(), + ]); + + // Remove subscription from registered topic subscriptions + _registeredTopicSubscriptions.remove(thisTopicSubscription); + + case SubscriptionStatus.internalServerError: + throw const PipeServerException(); + case SubscriptionStatus.invalid: + case SubscriptionStatus.malformed: + throw StateError('Internal Pipe error'); + case SubscriptionStatus.unauthorized: + throw StateError('Unsubscribes should not need authorization'); + } + } + + Future _reconnect(TopicSubscription topicSubscription) async { + final topic = topicSubscription.topic; + final state = topicSubscription.stateSubject.value; + + if (state is! TopicSubscriptionSubscribed) { + // Skip if not subscribed + return false; + } + + final reconnectingState = TopicSubscriptionReconnecting(); + topicSubscription.stateSubject.add(reconnectingState); + + for (var i = 0; i < _topicSubscriptionReconnectsCount; i++) { + // Register topic subscription in backend service + try { + final result = await _sendPipeServiceMethod( + topic: topic, + subscriptionId: topicSubscription.subscriptionId, + completer: reconnectingState.completer, + method: _PipeServiceMethod.subscribe, + // SubscriptionStatus type in this case can be ignored + onTimeout: () => SubscriptionStatus.invalid, + ); + + if (result == SubscriptionStatus.success) { + _logger.info( + 'Successfully reconnected subscription: $topicSubscription', + ); + + topicSubscription.stateSubject.add(state); + + for (final sub in state.registeredSubscriptions) { + sub.onReconnect?.call(); + } + + return true; + } + + // This reconnecting state completer is already completed so we have to + // send an another reconnecting state into the states subject + topicSubscription.stateSubject.add(TopicSubscriptionReconnecting()); + } catch (err, st) { + _logger.info( + 'Could not reconnect on ${i + 1} attempt, subscription: $topicSubscription', + err, + st, + ); + } + } + + // If failed to reconnect then add an error to the stream and close the controller + topicSubscription.stateSubject.addError(const PipeReconnectException()); + await Future.wait([ + topicSubscription.close(), + for (final sub in state.registeredSubscriptions) + () async { + sub.controller.addError(const PipeReconnectException()); + await sub.controller.close(); + state.registeredSubscriptions.remove(sub); + }(), + ]); + + // Subscription is no longer maintained so remove it from registered subscriptions + _registeredTopicSubscriptions.remove(topicSubscription); + + return false; + } + + Future _onSubscriptionResult(List? args) async { + final subscriptionData = args?.firstOrNull as Map?; + if (subscriptionData != null) { + final subscriptionResult = SubscriptionResult.fromJson(subscriptionData); + + final subscription = _registeredTopicSubscriptions.firstWhereOrNull( + (e) => e.subscriptionId == subscriptionResult.subscriptionId, + ); + + if (subscription == null) { + _logger.warning( + 'Could not find subscription for subscriptionId: ${subscriptionResult.subscriptionId}', + ); + return; + } + + _logger.finest('Received subscription: $subscriptionResult'); + + final state = subscription.stateSubject.value; + + switch (subscriptionResult.type) { + case OperationType.subscribe: + switch (state) { + case TopicSubscriptionUnsubscribing(): + case TopicSubscriptionSubscribed(): + throw StateError('Internal Pipe error'); + case TopicSubscriptionSubscribing(): + state.completer.complete(subscriptionResult.status); + return; + case TopicSubscriptionReconnecting(): + state.completer.complete(subscriptionResult.status); + return; + } + case OperationType.unsubscribe: + switch (state) { + case TopicSubscriptionUnsubscribing(): + // Async operation called before completer completes + await Future.wait([ + for (final sub in state.subscribedState.registeredSubscriptions) + sub.controller.close(), + ]); + state.completer.complete(subscriptionResult.status); + return; + case TopicSubscriptionSubscribed(): + case TopicSubscriptionSubscribing(): + case TopicSubscriptionReconnecting(): + throw StateError('Internal Pipe error'); + } + } + } + } + + void _onNotify(List? args) { + final notificationData = args?.firstOrNull as Map?; + if (notificationData != null) { + final envelope = NotificationEnvelope.fromJson(notificationData); + + _logger.finest('Received notifciation: $envelope'); + + final topicTypeSubscriptions = _registeredTopicSubscriptions + .where((e) => e.topic.getFullName() == envelope.topicType); + if (topicTypeSubscriptions.isEmpty) { + _logger.fine( + 'Could not match received notification to parent topic. ' + 'Type: ${envelope.topicType}', + ); + return; + } + + final subscription = topicTypeSubscriptions.firstWhereOrNull((e) { + final topicFromEnvelope = e.topic.fromJson(envelope.topic); + return topicFromEnvelope == e.topic; + }); + if (subscription == null) { + _logger.fine( + 'Received notification topic was not equal to registered subscription topic' + 'Topic: ${envelope.topic}', + ); + return; + } + + final state = subscription.stateSubject.value; + switch (state) { + case TopicSubscriptionSubscribed(): + final notification = subscription.topic.castNotification( + envelope.notificationType, + envelope.notification, + ); + + if (notification != null) { + for (final sub in state.registeredSubscriptions) { + sub.controller.add(notification); + } + } + return; + case TopicSubscriptionUnsubscribing(): + case TopicSubscriptionSubscribing(): + case TopicSubscriptionReconnecting(): + // We can do nothing with that information + return; + } + } + } + + Future dispose() async { + await Future.wait(_registeredTopicSubscriptions.map((e) => e.close())); + await _hubConnection.stop(); + } + + Future _sendPipeServiceMethod({ + required String subscriptionId, + required Topic topic, + required Completer completer, + required _PipeServiceMethod method, + FutureOr Function()? onTimeout, + }) async { + final envelope = SubscriptionEnvelope( + id: subscriptionId, + topic: jsonEncode(topic.toJson()), + topicType: topic.getFullName(), + ); + + // Register topic subscription in backend service + await _hubConnection.send( + methodName: method.methodName, + args: [envelope], + ); + + final result = await completer.future.catchError( + (dynamic errorOrException) { + if (errorOrException is Exception) { + throw errorOrException; + } else { + throw errorOrException as Error; + } + }, + ).timeout( + _signalRRequestTimeoutDuration, + onTimeout: onTimeout, + ); + + return result; + } +} + +enum _PipeServiceMethod { + subscribe('SubscribeAsync'), + unsubscribe('UnsubscribeAsync'); + + const _PipeServiceMethod(this.methodName); + + final String methodName; +} diff --git a/dart/lib/leancode_pipe/pipe_connection_state.dart b/dart/lib/leancode_pipe/pipe_connection_state.dart new file mode 100644 index 0000000..be0d7ec --- /dev/null +++ b/dart/lib/leancode_pipe/pipe_connection_state.dart @@ -0,0 +1,17 @@ +/// Describes the current state of the connection to the server. +enum PipeConnectionState { + /// The pipe connection is disconnected. + disconnected, + + /// The pipe connection is connecting. + connecting, + + /// The pipe connection is connected. + connected, + + /// The pipe connection is disconnecting. + disconnecting, + + /// The pipe connection is reconnecting. + reconnecting +} diff --git a/dart/lib/leancode_pipe/pipe_connection_state_mapper.dart b/dart/lib/leancode_pipe/pipe_connection_state_mapper.dart new file mode 100644 index 0000000..13ee507 --- /dev/null +++ b/dart/lib/leancode_pipe/pipe_connection_state_mapper.dart @@ -0,0 +1,16 @@ +import 'package:leancode_pipe/leancode_pipe/pipe_connection_state.dart'; +import 'package:signalr_core/signalr_core.dart'; + +abstract final class PipeConnectionStateMapper { + static PipeConnectionState fromHubConnectionState( + HubConnectionState? state, + ) => + switch (state) { + HubConnectionState.disconnected => PipeConnectionState.disconnected, + HubConnectionState.connecting => PipeConnectionState.connecting, + HubConnectionState.connected => PipeConnectionState.connected, + HubConnectionState.reconnecting => PipeConnectionState.reconnecting, + HubConnectionState.disconnecting => PipeConnectionState.disconnecting, + null => PipeConnectionState.disconnected, + }; +} diff --git a/dart/lib/leancode_pipe/pipe_error.dart b/dart/lib/leancode_pipe/pipe_error.dart new file mode 100644 index 0000000..45dc81f --- /dev/null +++ b/dart/lib/leancode_pipe/pipe_error.dart @@ -0,0 +1,15 @@ +class PipeConnectionException implements Exception { + const PipeConnectionException(); +} + +class PipeReconnectException implements Exception { + const PipeReconnectException(); +} + +class PipeUnauthorizedException implements Exception { + const PipeUnauthorizedException(); +} + +class PipeServerException implements Exception { + const PipeServerException(); +} diff --git a/dart/lib/leancode_pipe/pipe_subscription.dart b/dart/lib/leancode_pipe/pipe_subscription.dart new file mode 100644 index 0000000..a26f487 --- /dev/null +++ b/dart/lib/leancode_pipe/pipe_subscription.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +/// A structure representing subscription to a [Topic]. +class PipeSubscription + extends StreamView { + PipeSubscription({ + required Stream stream, + required Future Function() unsubscribe, + }) : _unsubscribe = unsubscribe, + super(stream); + + final Future Function() _unsubscribe; + + /// Unsubscribe to the topic. + Future unsubscribe() { + return _unsubscribe(); + } + + // Equality is based on references, do not override equality +} diff --git a/dart/lib/leancode_pipe/topic_subscription.dart b/dart/lib/leancode_pipe/topic_subscription.dart new file mode 100644 index 0000000..8b6f906 --- /dev/null +++ b/dart/lib/leancode_pipe/topic_subscription.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe/leancode_pipe/contracts/contracts.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:rxdart/rxdart.dart'; + +typedef TopicSubscribtionCompleter = Completer; + +class TopicSubscription { + TopicSubscription( + this.topic, + this.subscriptionId, + TopicSubscriptionState initState, + ) : stateSubject = BehaviorSubject.seeded(initState); + + @visibleForTesting + TopicSubscription.subject(this.topic, this.subscriptionId, this.stateSubject); + + final Topic topic; + final String subscriptionId; + final BehaviorSubject stateSubject; + + static final _logger = Logger('TopicSubscription'); + + bool _isClosed = false; + bool get isClosed => _isClosed; + + Future close() async { + final state = stateSubject.value; + + try { + await Future.wait([ + if (state is TopicSubscriptionSubscribed) + for (final sub in state.registeredSubscriptions) + sub.controller.close(), + stateSubject.close(), + ]); + + _isClosed = true; + } catch (err, st) { + _logger.shout('Could not cloes a topic subscription: $this', err, st); + + _isClosed = false; + } + } + + @override + String toString() => + 'TopicSubscription(topic: $topic, subscriptionId: $subscriptionId, stateSubject: $stateSubject)'; +} + +sealed class TopicSubscriptionState {} + +final class TopicSubscriptionSubscribing implements TopicSubscriptionState { + TopicSubscriptionSubscribing() : completer = TopicSubscribtionCompleter(); + + final TopicSubscribtionCompleter completer; +} + +class RegisteredSubscription { + RegisteredSubscription({ + required this.id, + required this.controller, + required Future Function() unsubscribe, + this.onReconnect, + }) : pipeSubscription = PipeSubscription( + stream: controller.stream, + unsubscribe: unsubscribe, + ); + + final String id; + final StreamController controller; + final PipeSubscription pipeSubscription; + final void Function()? onReconnect; +} + +final class TopicSubscriptionSubscribed + implements TopicSubscriptionState { + TopicSubscriptionSubscribed({ + required this.registeredSubscriptions, + }); + + final List registeredSubscriptions; +} + +final class TopicSubscriptionUnsubscribing + implements TopicSubscriptionState { + TopicSubscriptionUnsubscribing({required this.subscribedState}) + : completer = TopicSubscribtionCompleter(); + + final TopicSubscribtionCompleter completer; + final TopicSubscriptionSubscribed subscribedState; + + /// Count of the subscriptions requests that are waiting for unsubscribe() call + /// to complete. If [newSubscriptionRequestsCount] == 0, then after unsubscribe + /// this subscription will be removed from registered subscriptions, while otherwise + /// it will get registered back in the backend service. + int newSubscriptionRequestsCount = 0; +} + +final class TopicSubscriptionReconnecting implements TopicSubscriptionState { + TopicSubscriptionReconnecting() : completer = TopicSubscribtionCompleter(); + + final TopicSubscribtionCompleter completer; +} diff --git a/dart/pubspec.lock b/dart/pubspec.lock new file mode 100644 index 0000000..535fe03 --- /dev/null +++ b/dart/pubspec.lock @@ -0,0 +1,607 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + url: "https://pub.dev" + source: hosted + version: "8.6.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + leancode_lint: + dependency: "direct dev" + description: + name: leancode_lint + sha256: da5ae45a53e9cc78c7306ff4a292d266867bc81a7629b867f5c3d01d64c8939c + url: "https://pub.dev" + source: hosted + version: "6.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: "direct main" + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "9503969a7c2c78c7292022c70c0289ed6241df7a9ba720010c0b215af29a5a58" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + signalr_core: + dependency: "direct main" + description: + path: "." + ref: "feature/bump-dart-and-package-versions" + resolved-ref: d32848eaa8c0d0073c49cefa39feb20181fd44ad + url: "https://github.com/RCSandberg/signalr_core.git" + source: git + version: "1.1.1" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sse: + dependency: transitive + description: + name: sse + sha256: "3ff9088cac3f45aa8b91336f1962e3ea6c81baaba0bbba361c05f8aa7fb59442" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + sse_channel: + dependency: transitive + description: + path: "." + ref: "feature/bump-dart-and-package-versions" + resolved-ref: "50c4b76f2f37ee3d0dad6b7f46678482fdcc546c" + url: "https://github.com/RCSandberg/sse_channel.git" + source: git + version: "0.0.3" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.1.0 <4.0.0" diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml new file mode 100644 index 0000000..2a2d635 --- /dev/null +++ b/dart/pubspec.yaml @@ -0,0 +1,30 @@ +name: leancode_pipe +description: A Dart client for LeanCode's LeanPipe. +homepage: https://github.com/leancodepl/leanpipe/tree/main/dart +repository: https://github.com/leancodepl/leanpipe/tree/main/dart +publish_to: none # TODO: remove once publishable + +version: 0.1.0 + +environment: + sdk: ">=3.1.0 <=4.0.0" + +dependencies: + collection: ^1.17.1 + json_annotation: ^4.8.1 + logging: ^1.2.0 + meta: ^1.9.1 + rxdart: ^0.27.7 + # TODO: bring back pub version once merged and published + signalr_core: + git: + url: https://github.com/RCSandberg/signalr_core.git + ref: feature/bump-dart-and-package-versions + uuid: ^3.0.7 + +dev_dependencies: + build_runner: ^2.4.6 + json_serializable: ^6.7.1 + leancode_lint: ^6.0.0 + mocktail: ^1.0.0 + test: ^1.24.6 diff --git a/dart/test/leancode_pipe/common/mocks.dart b/dart/test/leancode_pipe/common/mocks.dart new file mode 100644 index 0000000..dbba4cd --- /dev/null +++ b/dart/test/leancode_pipe/common/mocks.dart @@ -0,0 +1,33 @@ +import 'package:leancode_pipe/leancode_pipe/contracts/topic.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_client.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:signalr_core/signalr_core.dart'; +import 'package:uuid/uuid.dart'; + +final class MockBehaviorSubject extends Mock + implements BehaviorSubject {} + +final class MockPipeClient extends Mock implements PipeClient {} + +final class MockHubConnection extends Mock implements HubConnection {} + +final class MockTopic extends Mock implements Topic { + MockTopic([this.id]); + + final String? id; + + @override + bool operator ==(covariant MockTopic other) { + if (identical(this, other)) { + return true; + } + + return getFullName() == other.getFullName() && toJson() == other.toJson(); + } + + @override + int get hashCode; +} + +final class MockUuid extends Mock implements Uuid {} diff --git a/dart/test/leancode_pipe/common/utils.dart b/dart/test/leancode_pipe/common/utils.dart new file mode 100644 index 0000000..5b1b34e --- /dev/null +++ b/dart/test/leancode_pipe/common/utils.dart @@ -0,0 +1,186 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe/leancode_pipe/contracts/contracts.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:signalr_core/signalr_core.dart'; +import 'package:uuid/uuid.dart'; + +import 'mocks.dart'; + +const pipeUrl = 'https://leanpipe-example.test.lncd.pl/pipe'; + +void prepareConnect(HubConnection connection) { + when(() => connection.onreconnected(any())).thenAnswer((_) {}); + when(() => connection.onreconnecting(any())).thenAnswer((_) {}); + when(() => connection.onclose(any())).thenAnswer((_) {}); + when(() => connection.on(any(), any())).thenAnswer((_) async {}); + when(() => connection.start()).thenAnswer((_) async {}); +} + +void prepareTopic(Topic topic) { + when(() => topic.getFullName()).thenReturn('name'); + when(() => topic.toJson()).thenReturn({}); +} + +typedef OnSubscriptionResult = Future Function(List args); +Future captureOnSubscriptionResult({ + required Future Function() connect, + required HubConnection connection, +}) async { + await connect(); + return verify( + () => connection.on( + 'subscriptionResult', + captureAny(), + ), + ).captured.first as OnSubscriptionResult; +} + +typedef OnReconnected = Future Function(String? connectionId); +Future captureOnReconnected({ + required Future Function() connect, + required HubConnection connection, +}) async { + await connect(); + return verify( + () => connection.onreconnected( + captureAny(), + ), + ).captured.first as OnReconnected; +} + +typedef OnClose = Future Function(Exception? exception); +Future captureOnClose({ + required Future Function() connect, + required HubConnection connection, +}) async { + await connect(); + return verify( + () => connection.onclose( + captureAny(), + ), + ).captured.first as OnClose; +} + +void prepareSubscribeToAnswerWithData({ + required HubConnection connection, + required Uuid uuid, + required Future Function(SubscriptionResult) answerCallback, +}) { + const id = 'subscriptionId'; + const subscriptionResult = SubscriptionResult( + type: OperationType.subscribe, + status: SubscriptionStatus.success, + subscriptionId: id, + ); + + // Simulate getting data from backend service with prepared subscription result + when(() => uuid.v1()).thenReturn(id); + when(() => uuid.v4()).thenReturn(id); + when( + () => connection.send( + methodName: 'SubscribeAsync', + args: any(named: 'args'), + ), + ).thenAnswer((_) async { + unawaited(answerCallback(subscriptionResult)); + }); +} + +void prepareUnsubscribeToAnswerWithData({ + required HubConnection connection, + required Uuid uuid, + required Future Function(SubscriptionResult) answerCallback, +}) { + const id = 'subscriptionId'; + const subscriptionResult = SubscriptionResult( + type: OperationType.unsubscribe, + status: SubscriptionStatus.success, + subscriptionId: id, + ); + + // Simulate getting data from backend service with prepared subscription result + when(() => uuid.v1()).thenReturn(id); + when(() => uuid.v4()).thenReturn(id); + when( + () => connection.send( + methodName: 'UnsubscribeAsync', + args: any(named: 'args'), + ), + ).thenAnswer((_) async { + unawaited(answerCallback(subscriptionResult)); + }); +} + +void verifySubscribeCalled(HubConnection connection, [int count = 1]) { + // Using verify on a mock resets its callCount for the method + Future callback() { + return connection.send( + methodName: 'SubscribeAsync', + args: any(named: 'args'), + ); + } + + if (count == 0) { + verifyNever(callback); + } else { + verify(callback).called(count); + } +} + +void verifyUnsubscribeCalled(HubConnection connection, [int count = 1]) { + // Using verify on a mock resets its callCount for the method + Future callback() { + return connection.send( + methodName: 'UnsubscribeAsync', + args: any(named: 'args'), + ); + } + + if (count == 0) { + verifyNever(callback); + } else { + verify(callback).called(count); + } +} + +void prepareSubscribeToAnswerWithDataPerTopic({ + required HubConnection connection, + required Uuid uuid, + required Future Function(SubscriptionResult) answerCallback, + required int count, +}) { + var i = -1; + // Simulate getting data from backend service with prepared subscription result + when(() => uuid.v1()).thenReturn('${i}_v1'); + when(() => uuid.v4()).thenAnswer((_) { + i++; + if (i == count) { + i = 0; + } + return i.toString(); + }); + + when( + () => connection.send( + methodName: 'SubscribeAsync', + args: any(named: 'args'), + ), + ).thenAnswer((invocation) async { + final args = + invocation.namedArguments[const Symbol('args')] as List; + final topicJson = jsonDecode((args.first as SubscriptionEnvelope).topic) + as Map; + final topicId = topicJson['id'] as String; + final topic = MockTopic(topicId); + + final result = SubscriptionResult( + type: OperationType.subscribe, + status: SubscriptionStatus.success, + subscriptionId: topic.id!, + ); + unawaited(answerCallback(result)); + }); +} diff --git a/dart/test/leancode_pipe/common/wait.dart b/dart/test/leancode_pipe/common/wait.dart new file mode 100644 index 0000000..f1333fa --- /dev/null +++ b/dart/test/leancode_pipe/common/wait.dart @@ -0,0 +1,7 @@ +Future wait({ + int seconds = 0, + int milliseconds = 0, +}) => + Future.delayed( + Duration(seconds: seconds, milliseconds: milliseconds), + ); diff --git a/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart b/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart new file mode 100644 index 0000000..8fe7f78 --- /dev/null +++ b/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart @@ -0,0 +1,172 @@ +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe/leancode_pipe/create_hub_connection.dart'; +import 'package:leancode_pipe/leancode_pipe/pipe_connection_state_mapper.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:signalr_core/signalr_core.dart'; +import 'package:test/test.dart'; +import 'package:uuid/uuid.dart'; + +import 'common/mocks.dart'; +import 'common/utils.dart'; + +void main() { + group('SignalR HubConnection tests', () { + late PipeClient client; + late HubConnection connection; + + setUp(() { + client = MockPipeClient(); + connection = createHubConnection(pipeUrl: pipeUrl); + }); + + test( + 'Underlying SignalR connection is initialized when connect() and' + 'closed when disconnect() called', () async { + when(() => client.connect()).thenAnswer((_) async => connection.start()); + when(() => client.disconnect()).thenAnswer( + (_) async => connection.stop(), + ); + when(() => client.connectionState).thenAnswer( + (_) => PipeConnectionStateMapper.fromHubConnectionState( + connection.state, + ), + ); + + expect(client.connectionState, PipeConnectionState.disconnected); + + await client.connect(); + + expect(client.connectionState, PipeConnectionState.connected); + + await client.disconnect(); + + expect(client.connectionState, PipeConnectionState.disconnected); + }); + }); + + group('PipeClient connection tests', () { + late HubConnection connection; + late Uuid uuid; + late PipeClient client; + late Topic topic; + + setUp(() { + connection = MockHubConnection(); + uuid = MockUuid(); + client = PipeClient.fromMock(hubConnection: connection, uuid: uuid); + topic = MockTopic(); + }); + + test( + 'PipeClient will only start internal connection if connection state ' + 'is disconnected', + () async { + final otherStates = HubConnectionState.values.toList() + ..remove(HubConnectionState.disconnected); + var currentState = otherStates.first; + + prepareConnect(connection); + when(() => connection.state).thenAnswer((_) => currentState); + + // Check other than disconnected states + for (final state in otherStates) { + currentState = state; + + await client.connect(); + verifyNever(() => connection.start()); + } + + // Check disconnected state + currentState = HubConnectionState.disconnected; + + await client.connect(); + + verify(() => connection.start()).called(1); + }, + ); + + test( + 'PipeClient will only disconnect if connection state is not disconnected', + () async { + final otherStates = HubConnectionState.values.toList() + ..remove(HubConnectionState.disconnected); + var currentState = HubConnectionState.disconnected; + + prepareConnect(connection); + when(() => connection.state).thenAnswer((_) => currentState); + when(() => connection.stop()).thenAnswer((_) async {}); + + // Check disconnected state + await client.disconnect(); + verifyNever(() => connection.stop()); + + // Check other than disconnected states + for (var i = 0; i < otherStates.length; i++) { + currentState = otherStates[i]; + + // After each verification, the number of method calls resets to 0 + verifyNever(connection.stop); + await client.disconnect(); + verify(connection.stop).called(1); + } + }, + ); + + test( + 'PipeClient disposes internal subscriptions on ' + 'disconnect() call', + () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // Prepare connection to simulate backend sending subscription confirmation + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) => + onSubscriptionResultMethod([subscribeResult.toJson()]), + ); + + // Get subscription with the same id as defined in `subscriptionResult` + final subscription = await client.subscribe(topic); + + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.connected); + + final onClose = await captureOnClose( + connect: client.connect, + connection: connection, + ); + when(() => connection.stop()).thenAnswer((_) async { + await onClose(null); + }); + + var isSubscriptionDone = false; + var isSubscriptionCanceled = false; + final sub = subscription + .doOnCancel(() => isSubscriptionDone = true) + .doOnCancel(() => isSubscriptionCanceled = true) + .listen((_) {}); + + // Check disconnected state + await client.disconnect(); + verify(() => connection.stop()).called(1); + + await sub.cancel(); + + expect(isSubscriptionDone, true); + expect(isSubscriptionCanceled, true); + }, + ); + }); +} diff --git a/dart/test/leancode_pipe/lean_pipe_client_test.dart b/dart/test/leancode_pipe/lean_pipe_client_test.dart new file mode 100644 index 0000000..89b0007 --- /dev/null +++ b/dart/test/leancode_pipe/lean_pipe_client_test.dart @@ -0,0 +1,711 @@ +// In many places we wait for TimeoutException from the pipe client. +// Currently default signalR timeout is set to 30s. We had to increase the +// default test timeout from 30s to 60s to let those TimeoutExceptions occur +@Timeout(Duration(seconds: 60)) +library; + +import 'dart:async'; + +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe/leancode_pipe/contracts/contracts.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:signalr_core/signalr_core.dart'; +import 'package:test/test.dart'; +import 'package:uuid/uuid.dart'; + +import 'common/mocks.dart'; +import 'common/utils.dart'; +import 'common/wait.dart'; + +void main() { + group('PipeClient subscribe() tests', () { + late HubConnection connection; + late Uuid uuid; + late PipeClient client; + late Topic topic; + + setUp(() { + connection = MockHubConnection(); + uuid = MockUuid(); + client = PipeClient.fromMock(hubConnection: connection, uuid: uuid); + topic = MockTopic(); + }); + + group('Topic not registered before', () { + test( + 'If topic is not registered before, subscribe() returns a valid ' + 'pipe subscription', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // Prepare connection to simulate backend sending subscription confirmation + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) => + onSubscriptionResultMethod([subscribeResult.toJson()]), + ); + + // Get subscription with the same id as defined in `subscriptionResult` + final subscription = await client.subscribe(topic); + + verifySubscribeCalled(connection); + expect(subscription, isA>()); + }); + + test( + 'If topic is not registered and backend service does not respond, ' + 'subscribe() throws a TimeoutException', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => uuid.v1()).thenReturn(''); + when(() => uuid.v4()).thenReturn(''); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Simulate no response from backend (no onSubscriptionResult call) + when( + () => connection.send( + methodName: 'SubscribeAsync', + args: any(named: 'args'), + ), + ).thenAnswer((_) async {}); + + await expectLater( + () async => client.subscribe(topic), + throwsA(const TypeMatcher()), + ); + }); + }); + + group('Topic already registered before', () { + test( + 'If there is an ongoing subscription for the topic then any ' + 'subscribe() calls for the same topic will only wait for the first ' + 'subscription to complete and return pipe subscription without calling' + 'the backend service', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // We want to answer to the subscription only once because internal logic + // should block any other subscription requests, waiting for the first + // subscription to complete instead + var isFirstAnswerToSubscribeRequest = true; + + // Prepare connection to simulate backend sending subscription confirmation + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + if (isFirstAnswerToSubscribeRequest) { + isFirstAnswerToSubscribeRequest = false; + + await wait(seconds: 4); + await onSubscriptionResultMethod([subscribeResult.toJson()]); + } + }, + ); + + await Future.wait([ + () async { + await client.subscribe(topic); + verifySubscribeCalled(connection); + }(), + for (var i = 0; i < 5; i++) + () async { + // Give first subscription time send request to backend service + await wait(milliseconds: 200); + // Id no longer required + await client.subscribe(topic); + verifySubscribeCalled(connection, 0); + }(), + ]); + }); + + test( + 'If there is an ongoing subscription for a topic, which will not ' + 'receive confirmation from backend, and an another subscription call ' + 'is made for that topic, the first subscription should throw a ' + 'TimeoutException and the second one should persue with the logic ' + '(try to connect to backend service)', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => uuid.v1()).thenReturn(''); + when(() => uuid.v4()).thenReturn(''); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + when( + () => connection.send( + methodName: 'SubscribeAsync', + args: any(named: 'args'), + ), + ).thenAnswer((_) async {}); + + await Future.wait([ + () async { + // First subscription + await expectLater( + () => client.subscribe(topic), + throwsA(const TypeMatcher()), + ); + verifySubscribeCalled(connection); + }(), + () async { + // Give first subscription time send request to backend service + await wait(seconds: 5); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = + await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) => + onSubscriptionResultMethod([subscribeResult.toJson()]), + ); + + await client.subscribe(topic); + + verifySubscribeCalled(connection); + }() + ]); + }); + + test( + 'If subscription for a specific topic is already registered then ' + 'on every further subscribe() call for that topic, return pipe ' + 'subscription without calling backend service', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // We want to answer to the subscription only once because internal logic + // should block any other subscription requests, waiting for the first + // subscription to complete instead + var isFirstAnswerToSubscribeRequest = true; + + // Prepare connection to simulate backend sending subscription confirmation + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + if (isFirstAnswerToSubscribeRequest) { + isFirstAnswerToSubscribeRequest = false; + + await onSubscriptionResultMethod([subscribeResult.toJson()]); + } + }, + ); + + // Get subscription with the same id as defined in `subscriptionResult` + await client.subscribe(topic); + verifySubscribeCalled(connection); + + await Future.wait([ + for (var i = 0; i < 5; i++) + () async { + await client.subscribe(topic); + // Verify that request to backend was sent only once + verifySubscribeCalled(connection, 0); + }(), + ]); + }); + }); + + test( + 'If subscription for a specific topic is in unsubscribing state, ' + 'the next subscribe() call for that topic, will wait until unsubscribing ' + 'is done, and persue with its logic to register a new subscription. ' + 'Other calls on subscribe() for that topic should as well wait for the ' + 'first one (after unsubscribe) finishes, and return pipe subscription ' + 'without calling backend', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Connect to catch on subscription result method + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + var subscriptionAnswerCounter = 0; + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + // Answer only to the first and seconds call: + // - First is to register subscription that we will unsubscribe later + // - Second is the first from many that we will register while topic + // subscription will be in unsubscribing state + if (subscriptionAnswerCounter < 2) { + subscriptionAnswerCounter++; + + await onSubscriptionResultMethod([subscribeResult.toJson()]); + } + }, + ); + + // Start a connection to be able to unsubscribe + final subscription = await client.subscribe(topic); + verifySubscribeCalled(connection); + + // Prepare for unsubscribe + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (result) async { + await wait(seconds: 4); + await onSubscriptionResultMethod([result.toJson()]); + }, + ); + + await Future.wait([ + () async { + await subscription.unsubscribe(); + verifyUnsubscribeCalled(connection); + }(), + () async { + await wait(milliseconds: 100); + // First subscription + expect(await client.subscribe(topic), isA()); + verifySubscribeCalled(connection); + }(), + for (var i = 0; i < 4; i++) + () async { + // Give first subscription time send request to backend service + await wait(milliseconds: 200); + + expect(await client.subscribe(topic), isA()); + verifySubscribeCalled(connection, 0); + }(), + ]); + }); + }); + + group('PipeClient unsubscribe() tests', () { + late HubConnection connection; + late Uuid uuid; + late PipeClient client; + late Topic topic; + + setUp(() { + connection = MockHubConnection(); + uuid = MockUuid(); + client = PipeClient.fromMock(hubConnection: connection, uuid: uuid); + topic = MockTopic(); + }); + + test('Unsubscribes from a registered subscription', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + await onSubscriptionResultMethod([subscribeResult.toJson()]); + }, + ); + + // Start a connection to be able to unsubscribe + final subscription = await client.subscribe(topic); + verifySubscribeCalled(connection); + + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (unsubscribeResult) async { + await onSubscriptionResultMethod([unsubscribeResult.toJson()]); + }, + ); + + // Prepare expected test result + var isSubscriptionDone = false; + final streamSub = subscription.listen( + (_) {}, + onDone: () => isSubscriptionDone = true, + ); + + // Unsubscribe and expect the stream to be done + await subscription.unsubscribe(); + verifyUnsubscribeCalled(connection); + + await wait(milliseconds: 500); + + // Clean up + await streamSub.cancel(); + + expect(isSubscriptionDone, true); + }); + + test( + 'Throws a StateError if topic is not registered (when already ' + 'unsubscribed from that subscription)', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + await onSubscriptionResultMethod([subscribeResult.toJson()]); + }, + ); + + // Start a connection to be able to unsubscribe + final subscription = await client.subscribe(topic); + verifySubscribeCalled(connection); + + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (unsubscribeResult) async { + await onSubscriptionResultMethod([unsubscribeResult.toJson()]); + }, + ); + + // Unsubscribe for the first time - should pass + await subscription.unsubscribe(); + verifyUnsubscribeCalled(connection); + + // Unsubscribe for the second time - should throw a StateError + expect( + subscription.unsubscribe, + throwsA(const TypeMatcher()), + ); + }); + + test( + 'If there are x subscriptions registered on the same topic, ' + 'only the last one should call unsubscribe in the backend service', + () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + await onSubscriptionResultMethod([subscribeResult.toJson()]); + }, + ); + + // Prepare subscriptions + const subscriptionsCount = 5; + final subscriptions = []; + await Future.wait([ + for (var i = 0; i < subscriptionsCount; i++) + () async { + subscriptions.add(await client.subscribe(topic)); + }(), + ]); + verifySubscribeCalled(connection); + + // Last subscription defined separatelly only for better scenario + // visualisation, could be defined in subscriptions as well + final lastSubscription = await client.subscribe(topic); + + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (unsubscribeResult) async { + await onSubscriptionResultMethod([unsubscribeResult.toJson()]); + }, + ); + + // None of the subscriptions should call backend service because + // there still is one registered subscription + await Future.wait([ + for (final sub in subscriptions) sub.unsubscribe(), + ]); + verifyUnsubscribeCalled(connection, 0); + + // Verify that last subscription calls backend service + await lastSubscription.unsubscribe(); + verifyUnsubscribeCalled(connection); + }); + + test( + 'If unsubscribe() called while subscription is in reconnecting state ' + 'and while subscribtion call will finish successfully, ' + 'unsubscribe call should wait for subscription to finish', () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // Prepare subscription answer at first time immidiately, while later + // (for reconnect) will answer with delay + var shouldWaitBeforeAnswering = false; + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscribeResult) async { + if (shouldWaitBeforeAnswering) { + await wait(seconds: 5); + } + + await onSubscriptionResultMethod([subscribeResult.toJson()]); + + // Verify that unsubscribe didn't call backend service + // before the reconnect was completed + verifyUnsubscribeCalled(connection, 0); + }, + ); + + final subscription = await client.subscribe(topic); + + // Prepare reconnecting state scenario + final onReconnected = await captureOnReconnected( + connect: client.connect, + connection: connection, + ); + + // Wait with answering to give time to unsubscribe to persue with its logic + shouldWaitBeforeAnswering = true; + unawaited(onReconnected('')); + + // Unsubscribe should wait for reconnect to finish up and then persue with + // the logic + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (unsubscribeResult) async { + await onSubscriptionResultMethod([unsubscribeResult.toJson()]); + }, + ); + + // Unsubscribe and verify if it did work + await subscription.unsubscribe(); + verifyUnsubscribeCalled(connection); + }); + + test( + 'If unsubscribe() called while subscription is in reconnecting state ' + 'and while subscribtion call will fail, unsubscribe call should wait ' + 'for subscription to finish and NOT send a call to the backend service', + () async { + // Prepare general config + prepareConnect(connection); + prepareTopic(topic); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + + // Prepare subscription answer at first time immidiately, while later + // (for reconnect) will answer with delay + var isFirstAnswer = true; + prepareSubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (subscriptionSuccededResult) async { + final subscriptionFailedResult = SubscriptionResult( + subscriptionId: subscriptionSuccededResult.subscriptionId, + status: SubscriptionStatus.invalid, + type: subscriptionSuccededResult.type, + ); + + if (isFirstAnswer) { + await onSubscriptionResultMethod( + [subscriptionSuccededResult.toJson()], + ); + return; + } + + // Wait with answering to give time to unsubscribe to persue with its logic + await wait(seconds: 3); + await onSubscriptionResultMethod([subscriptionFailedResult.toJson()]); + + // Verify that unsubscribe didn't call backend service + // before the reconnect was completed (failed) + verifyUnsubscribeCalled(connection, 0); + }, + ); + + final subscription = await client.subscribe(topic); + + // Prepare reconnecting state scenario + final onReconnected = await captureOnReconnected( + connect: client.connect, + connection: connection, + ); + + isFirstAnswer = false; + unawaited(onReconnected('')); + + // Unsubscribe should wait for reconnect to finish up and then persue with + // the logic + prepareUnsubscribeToAnswerWithData( + connection: connection, + uuid: uuid, + answerCallback: (unsubscribeResult) async { + await onSubscriptionResultMethod([unsubscribeResult.toJson()]); + }, + ); + + // Unsubscribe and verify if it did not call backend service + // because failed to reconnect + await subscription.unsubscribe(); + verifyUnsubscribeCalled(connection, 0); + }); + }); + + group('PipeClient reconnect() tests', () { + late HubConnection connection; + late Uuid uuid; + late PipeClient client; + + setUp(() { + connection = MockHubConnection(); + uuid = MockUuid(); + client = PipeClient.fromMock(hubConnection: connection, uuid: uuid); + }); + + test( + 'Reconnect calls subscribtion on backend for every registered topic ' + 'subscription and triggers its onReconnect call', () async { + // Prepare general config + prepareConnect(connection); + when(() => connection.state) + .thenAnswer((_) => HubConnectionState.disconnected); + + // Prepare multiple topics + const subscriptionsCount = 5; + final topics = {}; + for (var i = 0; i < subscriptionsCount; i++) { + final topic = MockTopic('$i'); + when(topic.getFullName).thenReturn('name$i'); + when(topic.toJson).thenReturn({ + 'id': '$i', + }); + topics.addAll({topic: false}); + } + + // Prepare a subscription + final onSubscriptionResultMethod = await captureOnSubscriptionResult( + connect: client.connect, + connection: connection, + ); + prepareSubscribeToAnswerWithDataPerTopic( + connection: connection, + uuid: uuid, + count: subscriptionsCount, + answerCallback: (result) async { + await onSubscriptionResultMethod([result.toJson()]); + }, + ); + + await Future.wait([ + for (var i = 0; i < topics.length; i++) + () async { + final topic = topics.keys.elementAt(i); + + await client.subscribe( + topic, + onReconnect: () { + topics[topic] = true; + }, + ); + }(), + ]); + + verifySubscribeCalled(connection, topics.length); + + // Prepare reconnecting state scenario + final onReconnected = await captureOnReconnected( + connect: client.connect, + connection: connection, + ); + + await onReconnected(''); + + // Verify that onReconnected called backend service + // for every registered subscription for an unique topic + verifySubscribeCalled(connection, topics.length); + + // Verify that every subscription got its onReconnect called + for (final value in topics.values) { + expect(value, true); + } + }); + }); +} +/* +*reconnect* +- success scenario +- failed scenario + */ diff --git a/dart/test/leancode_pipe/topic_subscription_test.dart b/dart/test/leancode_pipe/topic_subscription_test.dart new file mode 100644 index 0000000..81d88cb --- /dev/null +++ b/dart/test/leancode_pipe/topic_subscription_test.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:leancode_pipe/leancode_pipe/contracts/topic.dart'; +import 'package:leancode_pipe/leancode_pipe/topic_subscription.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import 'common/mocks.dart'; + +void main() { + group( + 'TopicSubscription states stream controller tests', + () { + late Topic topic; + late TopicSubscription topicSubscription; + + setUp(() { + topic = MockTopic(); + topicSubscription = TopicSubscription( + topic, + 'id', + TopicSubscriptionSubscribing(), + ); + }); + + test( + 'TopicSubscription closes states stream controller when dispose() is called', + () async { + expect(topicSubscription.stateSubject.isClosed, false); + + await topicSubscription.close(); + + expect(topicSubscription.stateSubject.isClosed, true); + expect(topicSubscription.isClosed, true); + }, + ); + }, + ); + + group( + 'TopicSubscription subscribed state tests', + () { + late Topic topic; + late MockBehaviorSubject stateSubject; + late TopicSubscription topicSubscription; + late TopicSubscriptionSubscribed subscribedState; + + setUp(() { + topic = MockTopic(); + stateSubject = MockBehaviorSubject(); + topicSubscription = TopicSubscription.subject( + topic, + 'id', + stateSubject, + ); + + final registeredSubscriptions = List.generate(5, (_) { + final controller = StreamController.broadcast(); + return RegisteredSubscription( + id: 'id', + controller: controller, + unsubscribe: controller.close, + ); + }); + + subscribedState = TopicSubscriptionSubscribed( + registeredSubscriptions: registeredSubscriptions, + ); + }); + + test( + 'TopicSubscription in Subscribed state closes notification stream ' + 'controllers for all registered subscriptions, when dispose() is called', + () async { + when(stateSubject.close).thenAnswer((_) async {}); + when(() => topicSubscription.stateSubject.value) + .thenReturn(subscribedState); + + for (final sub in subscribedState.registeredSubscriptions) { + expect(sub.controller.isClosed, false); + } + + await topicSubscription.close(); + + for (final sub in subscribedState.registeredSubscriptions) { + expect(sub.controller.isClosed, true); + } + expect(topicSubscription.isClosed, true); + }, + ); + }, + ); +} From a96b51d1fb22fd194f3d16fea6469a10a75628c7 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:28:10 +0200 Subject: [PATCH 2/9] Add github actions workflows --- .github/workflows/dart_cd.yml | 30 ++++++++++++++++++++++ .github/workflows/dart_ci.yml | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 .github/workflows/dart_cd.yml create mode 100644 .github/workflows/dart_ci.yml diff --git a/.github/workflows/dart_cd.yml b/.github/workflows/dart_cd.yml new file mode 100644 index 0000000..86c1f18 --- /dev/null +++ b/.github/workflows/dart_cd.yml @@ -0,0 +1,30 @@ +name: dart publish + +on: + push: + tags: ["dart-v*"] + +jobs: + publish: + name: Publish to pub.dev + + runs-on: ubuntu-latest + + permissions: + id-token: write + + defaults: + run: + working-directory: dart + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: 3.1.2 + + - name: Publish + run: dart pub publish -f diff --git a/.github/workflows/dart_ci.yml b/.github/workflows/dart_ci.yml new file mode 100644 index 0000000..05405ef --- /dev/null +++ b/.github/workflows/dart_ci.yml @@ -0,0 +1,48 @@ +name: dart test + +on: + push: + branches: [main] + tags-ignore: ["dart-v*"] + paths: + - "dart/**" + pull_request: + branches: [main] + paths: + - "dart/**" + +jobs: + test: + name: Dart ${{ matrix.version }} + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: ["3.1.2"] + + defaults: + run: + working-directory: dart + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ matrix.version }} + + - name: Download pub dependencies + run: dart pub get + + - name: Run analyzer + run: dart analyze + + - name: Run tests + run: dart test + + - name: Dry run pub publish + # We don't want it to fail the CI, it's just to see how would `pub publish` behave. + run: dart pub publish --dry-run || true From dac172815b434487dd97d4e65a190d9b21dfcf04 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:37:07 +0200 Subject: [PATCH 3/9] Add missing changelog --- dart/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 dart/CHANGELOG.md diff --git a/dart/CHANGELOG.md b/dart/CHANGELOG.md new file mode 100644 index 0000000..51d1510 --- /dev/null +++ b/dart/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- Initial version From b4b33fae90912919ec2325953eceb2a9e09a2c3e Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:44:19 +0200 Subject: [PATCH 4/9] Add example --- dart/example/.gitignore | 3 + dart/example/.metadata | 45 ++ dart/example/README.md | 16 + dart/example/analysis_options.yaml | 5 + dart/example/android/.gitignore | 13 + dart/example/android/app/build.gradle | 72 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 35 + .../leancode_pipe_example/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + dart/example/android/build.gradle | 31 + dart/example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + dart/example/android/settings.gradle | 11 + dart/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + dart/example/ios/Flutter/Debug.xcconfig | 1 + dart/example/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 616 ++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + dart/example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + dart/example/ios/Runner/Info.plist | 51 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + dart/example/lib/app.dart | 153 ++++ dart/example/lib/auction_contracts.dart | 121 ++++ dart/example/lib/auction_contracts.g.dart | 35 + dart/example/lib/main.dart | 7 + dart/example/lib/screen_cubit.dart | 174 +++++ dart/example/lib/screen_cubit.freezed.dart | 423 +++++++++++ dart/example/pubspec.lock | 670 ++++++++++++++++++ dart/example/pubspec.yaml | 101 +++ 70 files changed, 3115 insertions(+) create mode 100644 dart/example/.gitignore create mode 100644 dart/example/.metadata create mode 100644 dart/example/README.md create mode 100644 dart/example/analysis_options.yaml create mode 100644 dart/example/android/.gitignore create mode 100644 dart/example/android/app/build.gradle create mode 100644 dart/example/android/app/src/debug/AndroidManifest.xml create mode 100644 dart/example/android/app/src/main/AndroidManifest.xml create mode 100644 dart/example/android/app/src/main/kotlin/com/example/leancode_pipe_example/MainActivity.kt create mode 100644 dart/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 dart/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 dart/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 dart/example/android/app/src/main/res/values-night/styles.xml create mode 100644 dart/example/android/app/src/main/res/values/styles.xml create mode 100644 dart/example/android/app/src/profile/AndroidManifest.xml create mode 100644 dart/example/android/build.gradle create mode 100644 dart/example/android/gradle.properties create mode 100644 dart/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 dart/example/android/settings.gradle create mode 100644 dart/example/ios/.gitignore create mode 100644 dart/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 dart/example/ios/Flutter/Debug.xcconfig create mode 100644 dart/example/ios/Flutter/Release.xcconfig create mode 100644 dart/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 dart/example/ios/Runner/AppDelegate.swift create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 dart/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 dart/example/ios/Runner/Info.plist create mode 100644 dart/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 dart/example/ios/RunnerTests/RunnerTests.swift create mode 100644 dart/example/lib/app.dart create mode 100644 dart/example/lib/auction_contracts.dart create mode 100644 dart/example/lib/auction_contracts.g.dart create mode 100644 dart/example/lib/main.dart create mode 100644 dart/example/lib/screen_cubit.dart create mode 100644 dart/example/lib/screen_cubit.freezed.dart create mode 100644 dart/example/pubspec.lock create mode 100644 dart/example/pubspec.yaml diff --git a/dart/example/.gitignore b/dart/example/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/dart/example/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/dart/example/.metadata b/dart/example/.metadata new file mode 100644 index 0000000..b41277d --- /dev/null +++ b/dart/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "367f9ea16bfae1ca451b9cc27c1366870b187ae2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: android + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: ios + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: linux + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: macos + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: web + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: windows + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/dart/example/README.md b/dart/example/README.md new file mode 100644 index 0000000..f96c36e --- /dev/null +++ b/dart/example/README.md @@ -0,0 +1,16 @@ +# leancode_pipe_example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/dart/example/analysis_options.yaml b/dart/example/analysis_options.yaml new file mode 100644 index 0000000..6343f69 --- /dev/null +++ b/dart/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:leancode_lint/analysis_options.yaml + +analyzer: + exclude: + - "**/*.g.dart" diff --git a/dart/example/android/.gitignore b/dart/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/dart/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/dart/example/android/app/build.gradle b/dart/example/android/app/build.gradle new file mode 100644 index 0000000..6037296 --- /dev/null +++ b/dart/example/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.example.leancode_pipe_example" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.leancode_pipe_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/dart/example/android/app/src/debug/AndroidManifest.xml b/dart/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/dart/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/dart/example/android/app/src/main/AndroidManifest.xml b/dart/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b3a2666 --- /dev/null +++ b/dart/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/dart/example/android/app/src/main/kotlin/com/example/leancode_pipe_example/MainActivity.kt b/dart/example/android/app/src/main/kotlin/com/example/leancode_pipe_example/MainActivity.kt new file mode 100644 index 0000000..78ed8bd --- /dev/null +++ b/dart/example/android/app/src/main/kotlin/com/example/leancode_pipe_example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.leancode_pipe_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml b/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/dart/example/android/app/src/main/res/drawable/launch_background.xml b/dart/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/dart/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/dart/example/android/app/src/main/res/values-night/styles.xml b/dart/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/dart/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/dart/example/android/app/src/main/res/values/styles.xml b/dart/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/dart/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/dart/example/android/app/src/profile/AndroidManifest.xml b/dart/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/dart/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/dart/example/android/build.gradle b/dart/example/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/dart/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/dart/example/android/gradle.properties b/dart/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/dart/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/dart/example/android/gradle/wrapper/gradle-wrapper.properties b/dart/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/dart/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/dart/example/android/settings.gradle b/dart/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/dart/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/dart/example/ios/.gitignore b/dart/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/dart/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/dart/example/ios/Flutter/AppFrameworkInfo.plist b/dart/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/dart/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/dart/example/ios/Flutter/Debug.xcconfig b/dart/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/dart/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/dart/example/ios/Flutter/Release.xcconfig b/dart/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/dart/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/dart/example/ios/Runner.xcodeproj/project.pbxproj b/dart/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f8a1540 --- /dev/null +++ b/dart/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 43VU2NR6Z3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 43VU2NR6Z3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 43VU2NR6Z3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.leancodePipeDraftMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e42adcb --- /dev/null +++ b/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/dart/example/ios/Runner/AppDelegate.swift b/dart/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/dart/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dart/example/ios/Runner/Base.lproj/Main.storyboard b/dart/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/dart/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dart/example/ios/Runner/Info.plist b/dart/example/ios/Runner/Info.plist new file mode 100644 index 0000000..ad48d24 --- /dev/null +++ b/dart/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Leancode Pipe Draft Mobile + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + leancode_pipe_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/dart/example/ios/Runner/Runner-Bridging-Header.h b/dart/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/dart/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/dart/example/ios/RunnerTests/RunnerTests.swift b/dart/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/dart/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/dart/example/lib/app.dart b/dart/example/lib/app.dart new file mode 100644 index 0000000..2f2566e --- /dev/null +++ b/dart/example/lib/app.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe_example/screen_cubit.dart'; +import 'package:provider/provider.dart'; + +class LeancodePipeDraftApp extends StatelessWidget { + const LeancodePipeDraftApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: HomeScreen(), + ); + } +} + +class HomeScreen extends HookWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final bidController = useTextEditingController(); + + return Provider( + create: (context) => PipeClient( + pipeUrl: ScreenCubit.leanPipeExampleUrl, + tokenFactory: () async => 'token', + ), + dispose: (context, value) => value.dispose(), + child: BlocProvider( + create: (context) => ScreenCubit(context.read()), + child: Scaffold( + appBar: AppBar(title: const Text('Leancode Pipe Draft')), + body: BlocBuilder( + builder: (context, state) => SafeArea( + minimum: + const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 16), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: SingleChildScrollView( + child: Text( + state.map( + connected: (state) => state.data, + disconnected: (state) => + 'Disconnected. Error: ${state.error}. Connecting: ${state.connecting}', + ), + textAlign: TextAlign.start, + ), + ), + ), + const Divider(height: 32, thickness: 2), + Row( + children: [ + Expanded( + child: TextFormField( + controller: bidController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () { + final value = int.tryParse(bidController.text); + if (value != null) { + context.read().placeBid(value); + } + }, + child: const Text('Place bid'), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: context.read().buy, + child: const Text('Buy'), + ), + ], + ), + const Divider(thickness: 2, height: 24), + Row( + children: [ + const Text('Authorized:'), + const SizedBox(width: 16), + Checkbox( + value: state.authorized, + onChanged: (_) => context + .read() + .switchAuthorization(), + ), + ], + ), + const Divider(thickness: 2, height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: context.read().subscribe, + child: const Text('Subscribe'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton( + onPressed: + context.read().unsubscribe, + child: const Text('Unsubscribe'), + ), + ), + ], + ), + const Divider(thickness: 2, height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: context.read().connect, + child: const Text('Connect'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton( + onPressed: context.read().disconnect, + child: const Text('Disconnect'), + ), + ), + ], + ), + ], + ), + if (state is DraftScreenDisconnected && state.connecting) + const Positioned.fill( + child: IgnorePointer( + child: Center(child: CircularProgressIndicator()), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/dart/example/lib/auction_contracts.dart b/dart/example/lib/auction_contracts.dart new file mode 100644 index 0000000..392e75c --- /dev/null +++ b/dart/example/lib/auction_contracts.dart @@ -0,0 +1,121 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:leancode_pipe/leancode_pipe.dart'; + +part 'auction_contracts.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.pascal) +class Auction with EquatableMixin implements Topic { + // ignore: avoid_positional_boolean_parameters + Auction(this.auctionId, this.authorized); + + @override + AuctionNotification? castNotification(String tag, dynamic json) => + switch (tag) { + 'LeanPipe.Example.Contracts.BidPlaced' => + AuctionNotificationBidPlaced.fromJson(json as Map), + 'LeanPipe.Example.Contracts.ItemSold' => + AuctionNotificationItemSold.fromJson(json as Map), + _ => null + } as AuctionNotification?; + + final String auctionId; + final bool authorized; + + @override + String getFullName() => 'LeanPipe.Example.Contracts.Auction'; + + @override + Map toJson() => _$AuctionToJson(this); + + @override + bool? get stringify => true; + + @override + List get props => [auctionId, authorized]; + + @override + Topic fromJson(Map json) => + _$AuctionFromJson(json); +} + +sealed class AuctionNotification {} + +@JsonSerializable(fieldRename: FieldRename.pascal) +class BidPlaced with EquatableMixin { + BidPlaced({ + required this.amount, + required this.user, + }); + + factory BidPlaced.fromJson(Map json) => + _$BidPlacedFromJson(json); + + final int amount; + + final String user; + + @override + bool? get stringify => true; + + @override + List get props => [amount, user]; + + Map toJson() => _$BidPlacedToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.pascal) +class ItemSold with EquatableMixin { + ItemSold({ + required this.buyer, + }); + + factory ItemSold.fromJson(Map json) => + _$ItemSoldFromJson(json); + + final String buyer; + + @override + bool? get stringify => true; + + @override + List get props => [buyer]; + + Map toJson() => _$ItemSoldToJson(this); +} + +class AuctionNotificationBidPlaced + with EquatableMixin + implements AuctionNotification { + const AuctionNotificationBidPlaced(this.value); + + factory AuctionNotificationBidPlaced.fromJson(Map json) => + AuctionNotificationBidPlaced(BidPlaced.fromJson(json)); + + final BidPlaced value; + Map toJson() => value.toJson(); + + @override + bool? get stringify => true; + + @override + List get props => [value]; +} + +class AuctionNotificationItemSold + with EquatableMixin + implements AuctionNotification { + const AuctionNotificationItemSold(this.value); + + factory AuctionNotificationItemSold.fromJson(Map json) => + AuctionNotificationItemSold(ItemSold.fromJson(json)); + + final ItemSold value; + Map toJson() => value.toJson(); + + @override + bool? get stringify => true; + + @override + List get props => [value]; +} diff --git a/dart/example/lib/auction_contracts.g.dart b/dart/example/lib/auction_contracts.g.dart new file mode 100644 index 0000000..9e7d5b0 --- /dev/null +++ b/dart/example/lib/auction_contracts.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auction_contracts.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Auction _$AuctionFromJson(Map json) => Auction( + json['AuctionId'] as String, + json['Authorized'] as bool, + ); + +Map _$AuctionToJson(Auction instance) => { + 'AuctionId': instance.auctionId, + 'Authorized': instance.authorized, + }; + +BidPlaced _$BidPlacedFromJson(Map json) => BidPlaced( + amount: json['Amount'] as int, + user: json['User'] as String, + ); + +Map _$BidPlacedToJson(BidPlaced instance) => { + 'Amount': instance.amount, + 'User': instance.user, + }; + +ItemSold _$ItemSoldFromJson(Map json) => ItemSold( + buyer: json['Buyer'] as String, + ); + +Map _$ItemSoldToJson(ItemSold instance) => { + 'Buyer': instance.buyer, + }; diff --git a/dart/example/lib/main.dart b/dart/example/lib/main.dart new file mode 100644 index 0000000..4f5b1d1 --- /dev/null +++ b/dart/example/lib/main.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; + +void main() { + runApp(const LeancodePipeDraftApp()); +} diff --git a/dart/example/lib/screen_cubit.dart b/dart/example/lib/screen_cubit.dart new file mode 100644 index 0000000..3480bd7 --- /dev/null +++ b/dart/example/lib/screen_cubit.dart @@ -0,0 +1,174 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dispose_scope/dispose_scope.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:http/http.dart'; +import 'package:leancode_pipe/leancode_pipe.dart'; +import 'package:leancode_pipe_example/auction_contracts.dart'; +import 'package:logging/logging.dart'; + +part 'screen_cubit.freezed.dart'; + +class ScreenCubit extends Cubit with DisposeScope { + ScreenCubit(this._pipeClient) + : super( + const DraftScreenState.disconnected(authorized: true), + ); + + final PipeClient _pipeClient; + + static const _exampleAppUrl = 'https://leanpipe-example.test.lncd.pl'; + static const leanPipeExampleUrl = '$_exampleAppUrl/pipe'; + + StreamSubscription? _auctionPipeSub; + + static final _logger = Logger('DraftScreenCubit'); + + PipeSubscription? subscription; + + Future connect() async { + emit( + DraftScreenState.disconnected( + connecting: true, + authorized: state.authorized, + ), + ); + try { + await _pipeClient.connect(); + + emit( + DraftScreenState.connected( + data: 'connected\n\n', + authorized: state.authorized, + ), + ); + } catch (err) { + emit( + DraftScreenState.disconnected( + error: DraftScreenError.unknown, + authorized: state.authorized, + ), + ); + } + } + + Future disconnect() async { + try { + await _pipeClient.disconnect(); + emit(DraftScreenState.disconnected(authorized: state.authorized)); + } catch (err) { + emit( + DraftScreenState.disconnected( + error: DraftScreenError.unknown, + authorized: state.authorized, + ), + ); + } + } + + void _emitWithDataIfConnected(String data) { + final state = this.state; + if (state is! DraftScreenConnected) { + return; + } + + emit( + state.copyWith( + data: '${state.data}$data\n\n', + ), + ); + } + + Future subscribe() async { + if (state is! DraftScreenConnected) { + return; + } + + subscription = await _pipeClient.subscribe( + Auction('', state.authorized), + onReconnect: () { + _logger.fine('Downloading data after subscription reconnect'); + }, + ); + _auctionPipeSub = subscription?.listen( + (notification) { + _emitWithDataIfConnected(notification.toString()); + }, + cancelOnError: true, + onDone: () => _emitWithDataIfConnected('Auction subscription finished'), + onError: (dynamic error) => _emitWithDataIfConnected( + 'Auction subscription caught error: $error;\nClosing the subscription\n\n', + ), + ); + } + + void switchAuthorization() { + emit(state.copyWith(authorized: !state.authorized)); + } + + Future unsubscribe() async { + final state = this.state; + if (state is! DraftScreenConnected) { + return; + } + + await subscription?.unsubscribe(); + } + + Future placeBid(int amount) async { + await Client().post( + Uri.parse( + 'https://leanpipe-example.test.lncd.pl/cqrs/command/LeanPipe.Example.Contracts.PlaceBid', + ), + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'AuctionId': '', + 'Amount': amount, + 'UserId': '', + }), + ); + } + + Future buy() async { + await Client().post( + Uri.parse( + 'https://leanpipe-example.test.lncd.pl/cqrs/command/LeanPipe.Example.Contracts.Buy', + ), + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'AuctionId': '', + 'UserId': '', + }), + ); + } + + @override + Future close() async { + await _auctionPipeSub?.cancel(); + return super.close(); + } +} + +@freezed +class DraftScreenState with _$DraftScreenState { + const factory DraftScreenState.connected({ + required String data, + required bool authorized, + }) = DraftScreenConnected; + const factory DraftScreenState.disconnected({ + required bool authorized, + @Default(false) bool connecting, + DraftScreenError? error, + }) = DraftScreenDisconnected; +} + +enum DraftScreenError { + unknown, + network, +} diff --git a/dart/example/lib/screen_cubit.freezed.dart b/dart/example/lib/screen_cubit.freezed.dart new file mode 100644 index 0000000..3b0076d --- /dev/null +++ b/dart/example/lib/screen_cubit.freezed.dart @@ -0,0 +1,423 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'screen_cubit.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$DraftScreenState { + bool get authorized => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(String data, bool authorized) connected, + required TResult Function( + bool authorized, bool connecting, DraftScreenError? error) + disconnected, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String data, bool authorized)? connected, + TResult? Function( + bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String data, bool authorized)? connected, + TResult Function(bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(DraftScreenConnected value) connected, + required TResult Function(DraftScreenDisconnected value) disconnected, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(DraftScreenConnected value)? connected, + TResult? Function(DraftScreenDisconnected value)? disconnected, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(DraftScreenConnected value)? connected, + TResult Function(DraftScreenDisconnected value)? disconnected, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $DraftScreenStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DraftScreenStateCopyWith<$Res> { + factory $DraftScreenStateCopyWith( + DraftScreenState value, $Res Function(DraftScreenState) then) = + _$DraftScreenStateCopyWithImpl<$Res, DraftScreenState>; + @useResult + $Res call({bool authorized}); +} + +/// @nodoc +class _$DraftScreenStateCopyWithImpl<$Res, $Val extends DraftScreenState> + implements $DraftScreenStateCopyWith<$Res> { + _$DraftScreenStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? authorized = null, + }) { + return _then(_value.copyWith( + authorized: null == authorized + ? _value.authorized + : authorized // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DraftScreenConnectedCopyWith<$Res> + implements $DraftScreenStateCopyWith<$Res> { + factory _$$DraftScreenConnectedCopyWith(_$DraftScreenConnected value, + $Res Function(_$DraftScreenConnected) then) = + __$$DraftScreenConnectedCopyWithImpl<$Res>; + @override + @useResult + $Res call({String data, bool authorized}); +} + +/// @nodoc +class __$$DraftScreenConnectedCopyWithImpl<$Res> + extends _$DraftScreenStateCopyWithImpl<$Res, _$DraftScreenConnected> + implements _$$DraftScreenConnectedCopyWith<$Res> { + __$$DraftScreenConnectedCopyWithImpl(_$DraftScreenConnected _value, + $Res Function(_$DraftScreenConnected) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = null, + Object? authorized = null, + }) { + return _then(_$DraftScreenConnected( + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as String, + authorized: null == authorized + ? _value.authorized + : authorized // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$DraftScreenConnected implements DraftScreenConnected { + const _$DraftScreenConnected({required this.data, required this.authorized}); + + @override + final String data; + @override + final bool authorized; + + @override + String toString() { + return 'DraftScreenState.connected(data: $data, authorized: $authorized)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DraftScreenConnected && + (identical(other.data, data) || other.data == data) && + (identical(other.authorized, authorized) || + other.authorized == authorized)); + } + + @override + int get hashCode => Object.hash(runtimeType, data, authorized); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DraftScreenConnectedCopyWith<_$DraftScreenConnected> get copyWith => + __$$DraftScreenConnectedCopyWithImpl<_$DraftScreenConnected>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String data, bool authorized) connected, + required TResult Function( + bool authorized, bool connecting, DraftScreenError? error) + disconnected, + }) { + return connected(data, authorized); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String data, bool authorized)? connected, + TResult? Function( + bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + }) { + return connected?.call(data, authorized); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String data, bool authorized)? connected, + TResult Function(bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + required TResult orElse(), + }) { + if (connected != null) { + return connected(data, authorized); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(DraftScreenConnected value) connected, + required TResult Function(DraftScreenDisconnected value) disconnected, + }) { + return connected(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(DraftScreenConnected value)? connected, + TResult? Function(DraftScreenDisconnected value)? disconnected, + }) { + return connected?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(DraftScreenConnected value)? connected, + TResult Function(DraftScreenDisconnected value)? disconnected, + required TResult orElse(), + }) { + if (connected != null) { + return connected(this); + } + return orElse(); + } +} + +abstract class DraftScreenConnected implements DraftScreenState { + const factory DraftScreenConnected( + {required final String data, + required final bool authorized}) = _$DraftScreenConnected; + + String get data; + @override + bool get authorized; + @override + @JsonKey(ignore: true) + _$$DraftScreenConnectedCopyWith<_$DraftScreenConnected> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$DraftScreenDisconnectedCopyWith<$Res> + implements $DraftScreenStateCopyWith<$Res> { + factory _$$DraftScreenDisconnectedCopyWith(_$DraftScreenDisconnected value, + $Res Function(_$DraftScreenDisconnected) then) = + __$$DraftScreenDisconnectedCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool authorized, bool connecting, DraftScreenError? error}); +} + +/// @nodoc +class __$$DraftScreenDisconnectedCopyWithImpl<$Res> + extends _$DraftScreenStateCopyWithImpl<$Res, _$DraftScreenDisconnected> + implements _$$DraftScreenDisconnectedCopyWith<$Res> { + __$$DraftScreenDisconnectedCopyWithImpl(_$DraftScreenDisconnected _value, + $Res Function(_$DraftScreenDisconnected) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? authorized = null, + Object? connecting = null, + Object? error = freezed, + }) { + return _then(_$DraftScreenDisconnected( + authorized: null == authorized + ? _value.authorized + : authorized // ignore: cast_nullable_to_non_nullable + as bool, + connecting: null == connecting + ? _value.connecting + : connecting // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as DraftScreenError?, + )); + } +} + +/// @nodoc + +class _$DraftScreenDisconnected implements DraftScreenDisconnected { + const _$DraftScreenDisconnected( + {required this.authorized, this.connecting = false, this.error}); + + @override + final bool authorized; + @override + @JsonKey() + final bool connecting; + @override + final DraftScreenError? error; + + @override + String toString() { + return 'DraftScreenState.disconnected(authorized: $authorized, connecting: $connecting, error: $error)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DraftScreenDisconnected && + (identical(other.authorized, authorized) || + other.authorized == authorized) && + (identical(other.connecting, connecting) || + other.connecting == connecting) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, authorized, connecting, error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DraftScreenDisconnectedCopyWith<_$DraftScreenDisconnected> get copyWith => + __$$DraftScreenDisconnectedCopyWithImpl<_$DraftScreenDisconnected>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String data, bool authorized) connected, + required TResult Function( + bool authorized, bool connecting, DraftScreenError? error) + disconnected, + }) { + return disconnected(authorized, connecting, error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String data, bool authorized)? connected, + TResult? Function( + bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + }) { + return disconnected?.call(authorized, connecting, error); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String data, bool authorized)? connected, + TResult Function(bool authorized, bool connecting, DraftScreenError? error)? + disconnected, + required TResult orElse(), + }) { + if (disconnected != null) { + return disconnected(authorized, connecting, error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(DraftScreenConnected value) connected, + required TResult Function(DraftScreenDisconnected value) disconnected, + }) { + return disconnected(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(DraftScreenConnected value)? connected, + TResult? Function(DraftScreenDisconnected value)? disconnected, + }) { + return disconnected?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(DraftScreenConnected value)? connected, + TResult Function(DraftScreenDisconnected value)? disconnected, + required TResult orElse(), + }) { + if (disconnected != null) { + return disconnected(this); + } + return orElse(); + } +} + +abstract class DraftScreenDisconnected implements DraftScreenState { + const factory DraftScreenDisconnected( + {required final bool authorized, + final bool connecting, + final DraftScreenError? error}) = _$DraftScreenDisconnected; + + @override + bool get authorized; + bool get connecting; + DraftScreenError? get error; + @override + @JsonKey(ignore: true) + _$$DraftScreenDisconnectedCopyWith<_$DraftScreenDisconnected> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/dart/example/pubspec.lock b/dart/example/pubspec.lock new file mode 100644 index 0000000..32b511d --- /dev/null +++ b/dart/example/pubspec.lock @@ -0,0 +1,670 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + url: "https://pub.dev" + source: hosted + version: "8.1.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + url: "https://pub.dev" + source: hosted + version: "2.4.6" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + url: "https://pub.dev" + source: hosted + version: "8.6.3" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + collection: + dependency: transitive + description: + name: collection + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" + source: hosted + version: "1.17.2" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + dispose_scope: + dependency: "direct main" + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + url: "https://pub.dev" + source: hosted + version: "8.1.3" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "6ae13b1145c589112cbd5c4fda6c65908993a9cb18d4f82042e9c28dd9fbf611" + url: "https://pub.dev" + source: hosted + version: "0.20.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "83462cfc33dc9680533a7f3a4a6ab60aa94f287db5f4ee6511248c22833c497f" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" + source: hosted + version: "6.7.1" + leancode_pipe: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.0" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + signalr_core: + dependency: transitive + description: + path: "." + ref: "feature/bump-dart-and-package-versions" + resolved-ref: d32848eaa8c0d0073c49cefa39feb20181fd44ad + url: "https://github.com/RCSandberg/signalr_core.git" + source: git + version: "1.1.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sse: + dependency: transitive + description: + name: sse + sha256: "3ff9088cac3f45aa8b91336f1962e3ea6c81baaba0bbba361c05f8aa7fb59442" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + sse_channel: + dependency: transitive + description: + path: "." + ref: "feature/bump-dart-and-package-versions" + resolved-ref: "50c4b76f2f37ee3d0dad6b7f46678482fdcc546c" + url: "https://github.com/RCSandberg/sse_channel.git" + source: git + version: "0.0.3" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.1.2 <4.0.0" + flutter: ">=3.0.0" diff --git a/dart/example/pubspec.yaml b/dart/example/pubspec.yaml new file mode 100644 index 0000000..21ff81d --- /dev/null +++ b/dart/example/pubspec.yaml @@ -0,0 +1,101 @@ +name: leancode_pipe_example +description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ">=3.1.2 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + leancode_pipe: + path: .. + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + equatable: ^2.0.5 + json_annotation: ^4.8.1 + flutter_bloc: ^8.1.3 + freezed_annotation: ^2.4.1 + http: ^1.1.0 + logging: ^1.2.0 + dispose_scope: ^2.1.0 + flutter_hooks: ^0.20.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + build_runner: ^2.4.6 + json_serializable: ^6.7.1 + freezed: ^2.4.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages From 50f6f160fb1622ab9466b329a2a52fad243f4cf8 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:44:32 +0200 Subject: [PATCH 5/9] Rename fullName to tag in castNotification --- dart/lib/leancode_pipe/contracts/topic.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/leancode_pipe/contracts/topic.dart b/dart/lib/leancode_pipe/contracts/topic.dart index b03b3d5..a026abb 100644 --- a/dart/lib/leancode_pipe/contracts/topic.dart +++ b/dart/lib/leancode_pipe/contracts/topic.dart @@ -1,5 +1,5 @@ abstract interface class Topic { - Notification? castNotification(String fullName, dynamic json); + Notification? castNotification(String tag, dynamic json); String getFullName(); From ba35bf65bdd7623ff3036c6623b99b15206d49fc Mon Sep 17 00:00:00 2001 From: shilangyu Date: Mon, 25 Sep 2023 16:47:42 +0200 Subject: [PATCH 6/9] Add build folder to gitignore --- dart/example/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/dart/example/.gitignore b/dart/example/.gitignore index 3a85790..4dda46f 100644 --- a/dart/example/.gitignore +++ b/dart/example/.gitignore @@ -1,3 +1,4 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ +build/ From b54ed3e5c0431d2ac2ee684557b507dd0554810b Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 3 Oct 2023 13:46:40 +0200 Subject: [PATCH 7/9] Fix leftovers --- dart/README.md | 2 ++ dart/lib/leancode_pipe/pipe_client.dart | 2 +- .../test/leancode_pipe/lean_pipe_client_connection_test.dart | 2 +- dart/test/leancode_pipe/lean_pipe_client_test.dart | 5 ----- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/dart/README.md b/dart/README.md index 7b82329..ea49134 100644 --- a/dart/README.md +++ b/dart/README.md @@ -10,3 +10,5 @@ Add these into your AndroidManifest.xml on the manifest level ``` + +TODO: docs diff --git a/dart/lib/leancode_pipe/pipe_client.dart b/dart/lib/leancode_pipe/pipe_client.dart index db28bc4..a0221b3 100644 --- a/dart/lib/leancode_pipe/pipe_client.dart +++ b/dart/lib/leancode_pipe/pipe_client.dart @@ -94,7 +94,6 @@ class PipeClient { }) ..on('subscriptionResult', _onSubscriptionResult) ..on('notify', _onNotify); - // FIXME: Verify what does it throw if authorization does not pass through (can be bugged on backend) await _hubConnection.start(); } catch (err, st) { _logger.shout('Could not connect to LeanCode pipe service', err, st); @@ -215,6 +214,7 @@ class PipeClient { required void Function()? onReconnect, }) { final controller = StreamController.broadcast(); + // v1 is used for easier mocking in tests to differentiate between the internal IDs and server IDs final id = _uuid.v1(); final subscription = RegisteredSubscription( id: id, diff --git a/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart b/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart index 8fe7f78..0edf1b6 100644 --- a/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart +++ b/dart/test/leancode_pipe/lean_pipe_client_connection_test.dart @@ -154,7 +154,7 @@ void main() { var isSubscriptionDone = false; var isSubscriptionCanceled = false; final sub = subscription - .doOnCancel(() => isSubscriptionDone = true) + .doOnDone(() => isSubscriptionDone = true) .doOnCancel(() => isSubscriptionCanceled = true) .listen((_) {}); diff --git a/dart/test/leancode_pipe/lean_pipe_client_test.dart b/dart/test/leancode_pipe/lean_pipe_client_test.dart index 89b0007..1ffd2c0 100644 --- a/dart/test/leancode_pipe/lean_pipe_client_test.dart +++ b/dart/test/leancode_pipe/lean_pipe_client_test.dart @@ -704,8 +704,3 @@ void main() { }); }); } -/* -*reconnect* -- success scenario -- failed scenario - */ From a0db9bc1850698299bd7b75a8e30e68b7d90dda2 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 3 Oct 2023 14:13:43 +0200 Subject: [PATCH 8/9] Don't run pub get on example --- .github/workflows/dart_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dart_ci.yml b/.github/workflows/dart_ci.yml index 05405ef..2d8a455 100644 --- a/.github/workflows/dart_ci.yml +++ b/.github/workflows/dart_ci.yml @@ -35,7 +35,7 @@ jobs: sdk: ${{ matrix.version }} - name: Download pub dependencies - run: dart pub get + run: dart pub get --no-example - name: Run analyzer run: dart analyze From 3ec2bb86cfd33e60d397a854f99c875bc1cf5f6a Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 3 Oct 2023 14:16:10 +0200 Subject: [PATCH 9/9] Disable analyzer for example --- dart/analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/dart/analysis_options.yaml b/dart/analysis_options.yaml index 6343f69..d89987c 100644 --- a/dart/analysis_options.yaml +++ b/dart/analysis_options.yaml @@ -3,3 +3,4 @@ include: package:leancode_lint/analysis_options.yaml analyzer: exclude: - "**/*.g.dart" + - example/