From 31e54d0792cbcdb509f7fbf10433c5c023d415e7 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 28 Aug 2020 08:38:41 +0100 Subject: [PATCH 01/33] Release 1.0.28 - Create new release branch - Update references to 1.0.28 --- CHANGELOG.md | 2 ++ README.md | 2 +- lib/src/base/parse_constants.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14398f151..c17e5d3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## 1.0.28 + ## 1.0.27 User login / signUp / loginAnonymous delete SessionId stored in device before calling server diff --git a/README.md b/README.md index e30959fd3..2e45d7f2d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Want to get involved? Join our Slack channel and help out! (http://flutter-parse To install, either add to your pubspec.yaml ```yml dependencies: - parse_server_sdk: ^1.0.27 + parse_server_sdk: ^1.0.28 ``` or clone this repository and add to your project. As this is an early development with multiple contributors, it is probably best to download/clone and keep updating as an when a new feature is added. diff --git a/lib/src/base/parse_constants.dart b/lib/src/base/parse_constants.dart index 56a3d9962..b645de916 100644 --- a/lib/src/base/parse_constants.dart +++ b/lib/src/base/parse_constants.dart @@ -1,7 +1,7 @@ part of flutter_parse_sdk; // Library -const String keySdkVersion = '1.0.27'; +const String keySdkVersion = '1.0.28'; const String keyLibraryName = 'Flutter Parse SDK'; // End Points diff --git a/pubspec.yaml b/pubspec.yaml index 513807a96..ed3c92eac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: parse_server_sdk description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) -version: 1.0.27 +version: 1.0.28 homepage: https://github.com/phillwiggins/flutter_parse_sdk environment: From 7462a01a934c263ba91d67e3815438784987c1d4 Mon Sep 17 00:00:00 2001 From: Nils Strelow <1096841+nstrelow@users.noreply.github.com> Date: Sat, 29 Aug 2020 23:01:36 +0200 Subject: [PATCH 02/33] Replace devicelocale by Platform.localeName call (#430) * Replace devicelocale by Platform.localeName call * Use flutter_test to meet new requirements --- lib/parse_server_sdk.dart | 1 - lib/src/objects/parse_installation.dart | 2 +- pubspec.yaml | 4 ++-- test/parse_client_configuration_test.dart | 2 +- test/parse_query_test.dart | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 606fd131c..45b4b42dd 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -7,7 +7,6 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index 34e9eaa45..ad44b3470 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -85,7 +85,7 @@ class ParseInstallation extends ParseObject { //Locale final String locale = parseIsWeb ? ui.window.locale.toString() - : await Devicelocale.currentLocale; + : Platform.localeName; if (locale != null && locale.isNotEmpty) { set(keyLocaleIdentifier, locale); } diff --git a/pubspec.yaml b/pubspec.yaml index ed3c92eac..b23f9ab4a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,11 +24,11 @@ dependencies: path_provider: ^1.6.14 uuid: ^2.2.2 package_info: ^0.4.3 - devicelocale: ^0.3.1 meta: ^1.1.8 path: ^1.7.0 dev_dependencies: # Testing - test: ^1.15.3 + flutter_test: + sdk: flutter mockito: ^4.1.1 diff --git a/test/parse_client_configuration_test.dart b/test/parse_client_configuration_test.dart index 237c7fd79..94c8b1e8c 100644 --- a/test/parse_client_configuration_test.dart +++ b/test/parse_client_configuration_test.dart @@ -1,6 +1,6 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:parse_server_sdk/parse_server_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:test/test.dart'; void main() { SharedPreferences.setMockInitialValues(Map()); diff --git a/test/parse_query_test.dart b/test/parse_query_test.dart index 61c67005b..7506f095f 100644 --- a/test/parse_query_test.dart +++ b/test/parse_query_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:parse_server_sdk/parse_server_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:test/test.dart'; class MockClient extends Mock implements ParseHTTPClient {} From efbb9c68ea8b329860e7320eace8879c6bb41ea4 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Tue, 1 Sep 2020 09:30:36 +0200 Subject: [PATCH 03/33] Livequery: combine both implementations (#433) * make livequeries web implementation more similar to the mobile implementation * combining both livequery implementations * make livequery "part of flutter_parse_sdk" * smal fix --- lib/parse_server_sdk.dart | 8 +- lib/src/network/parse_live_query.dart | 76 ++-- lib/src/network/parse_live_query_web.dart | 462 ---------------------- lib/src/network/parse_websocket.dart | 2 + lib/src/network/parse_websocket_html.dart | 33 ++ lib/src/network/parse_websocket_io.dart | 31 ++ 6 files changed, 104 insertions(+), 508 deletions(-) delete mode 100644 lib/src/network/parse_live_query_web.dart create mode 100644 lib/src/network/parse_websocket.dart create mode 100644 lib/src/network/parse_websocket_html.dart create mode 100644 lib/src/network/parse_websocket_io.dart diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 45b4b42dd..681143501 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -7,20 +7,23 @@ import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:connectivity/connectivity.dart'; +import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:http/io_client.dart'; import 'package:meta/meta.dart'; import 'package:package_info/package_info.dart'; +import 'package:parse_server_sdk/src/network/parse_websocket.dart' + as parse_web_socket; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:sembast/sembast.dart'; import 'package:sembast/sembast_io.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:xxtea/xxtea.dart'; -export 'src/network/parse_live_query.dart' - if (dart.library.js) 'src/network/parse_live_query_web.dart'; export 'src/utils/parse_live_list.dart'; part 'package:parse_server_sdk/src/data/core_store.dart'; @@ -37,6 +40,7 @@ part 'src/base/parse_constants.dart'; part 'src/data/parse_core_data.dart'; part 'src/enums/parse_enum_api_rq.dart'; part 'src/network/parse_http_client.dart'; +part 'src/network/parse_live_query.dart'; part 'src/network/parse_query.dart'; part 'src/objects/parse_acl.dart'; part 'src/objects/parse_base.dart'; diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index ca90110c0..05e6be9a4 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -1,13 +1,4 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:connectivity/connectivity.dart'; -import 'package:flutter/widgets.dart'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -import '../../parse_server_sdk.dart'; +part of flutter_parse_sdk; enum LiveQueryEvent { create, enter, update, leave, delete, error } @@ -31,7 +22,6 @@ class Subscription { 'error' ]; Map eventCallbacks = {}; - void on(LiveQueryEvent op, Function callback) { eventCallbacks[_liveQueryEvent[op.index]] = callback; } @@ -46,9 +36,13 @@ enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } class LiveQueryReconnectingController with WidgetsBindingObserver { LiveQueryReconnectingController( this._reconnect, this._eventStream, this.debug) { - Connectivity().checkConnectivity().then(_connectivityChanged); - Connectivity().onConnectivityChanged.listen(_connectivityChanged); - + //Connectivity works differently on web + if (!parseIsWeb) { + Connectivity().checkConnectivity().then(_connectivityChanged); + Connectivity().onConnectivityChanged.listen(_connectivityChanged); + } else { + _connectivityChanged(ConnectivityResult.wifi); + } _eventStream.listen((LiveQueryClientEvent event) { switch (event) { case LiveQueryClientEvent.CONNECTED: @@ -132,10 +126,9 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { } } -class Client { - factory Client() => _getInstance(); - - Client._internal( +class LiveQueryClient { + factory LiveQueryClient() => _getInstance(); + LiveQueryClient._internal( {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { _clientEventStreamController = StreamController(); _clientEventStream = @@ -160,13 +153,11 @@ class Client { reconnectingController = LiveQueryReconnectingController( () => reconnect(userInitialized: false), getClientEventStream, _debug); } - - static Client get instance => _getInstance(); - static Client _instance; - - static Client _getInstance( + static LiveQueryClient get instance => _getInstance(); + static LiveQueryClient _instance; + static LiveQueryClient _getInstance( {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { - _instance ??= Client._internal( + _instance ??= LiveQueryClient._internal( debug: debug, client: client, autoSendSessionId: autoSendSessionId); return _instance; } @@ -175,7 +166,7 @@ class Client { return _clientEventStream; } - WebSocket _webSocket; + parse_web_socket.WebSocket _webSocket; ParseHTTPClient _client; bool _debug; bool _sendSessionId; @@ -186,7 +177,6 @@ class Client { Stream _clientEventStream; LiveQueryReconnectingController reconnectingController; - // ignore: always_specify_types final Map _requestSubScription = {}; Future reconnect({bool userInitialized = false}) async { @@ -198,11 +188,12 @@ class Client { if (_webSocket != null) { return _webSocket.readyState; } - return WebSocket.connecting; + return parse_web_socket.WebSocket.CONNECTING; } Future disconnect({bool userInitialized = false}) async { - if (_webSocket != null && _webSocket.readyState == WebSocket.open) { + if (_webSocket != null && + _webSocket.readyState == parse_web_socket.WebSocket.OPEN) { if (_debug) { print('$_printConstLiveQuery: Socket closed'); } @@ -216,7 +207,6 @@ class Client { await _channel.sink.close(); _channel = null; } - // ignore: always_specify_types _requestSubScription.values.toList().forEach((Subscription subscription) { subscription._enabled = false; }); @@ -274,9 +264,10 @@ class Client { _connecting = true; try { - _webSocket = await WebSocket.connect(_liveQueryURL); + _webSocket = await parse_web_socket.WebSocket.connect(_liveQueryURL); _connecting = false; - if (_webSocket != null && _webSocket.readyState == WebSocket.open) { + if (_webSocket != null && + _webSocket.readyState == parse_web_socket.WebSocket.OPEN) { if (_debug) { print('$_printConstLiveQuery: Socket opened'); } @@ -286,7 +277,7 @@ class Client { } return Future.value(null); } - _channel = IOWebSocketChannel(_webSocket); + _channel = _webSocket.createWebSocketChannel(); _channel.stream.listen((dynamic message) { _handleMessage(message); }, onDone: () { @@ -302,8 +293,11 @@ class Client { print( '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); } - return Future.value(handleException(Exception(error), - ParseApiRQ.liveQuery, _debug, 'IOWebSocketChannel')); + return Future.value(handleException( + Exception(error), + ParseApiRQ.liveQuery, + _debug, + !parseIsWeb ? 'IOWebSocketChannel' : 'HtmlWebSocketChannel')); }); } on Exception catch (e) { _connecting = false; @@ -341,13 +335,11 @@ class Client { _channel.sink.add(jsonEncode(connectMessage)); } - // ignore: always_specify_types void _subscribeLiveQuery(Subscription subscription) { if (subscription._enabled) { return; } subscription._enabled = true; - // ignore: always_specify_types final QueryBuilder query = subscription.query; final List keysToReturn = query.limiters['keys']?.split(','); query.limiters.clear(); //Remove limits in LiveQuery @@ -386,11 +378,11 @@ class Client { } final Map actionData = jsonDecode(message); - // ignore: always_specify_types + Subscription subscription; if (actionData.containsKey('op') && actionData['op'] == 'connected') { print('ReSubScription:$_requestSubScription'); - // ignore: always_specify_types + _requestSubScription.values.toList().forEach((Subscription subcription) { _subscribeLiveQuery(subcription); }); @@ -436,21 +428,17 @@ class LiveQuery { _debug = isDebugEnabled(objectLevelDebug: debug); _sendSessionId = autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; - this.client = Client._getInstance( + this.client = LiveQueryClient._getInstance( client: _client, debug: _debug, autoSendSessionId: _sendSessionId); } ParseHTTPClient _client; bool _debug; bool _sendSessionId; - - // ignore: always_specify_types Subscription _latestSubscription; - Client client; + LiveQueryClient client; - // ignore: always_specify_types @deprecated - // ignore: always_specify_types Future subscribe(QueryBuilder query) async { _latestSubscription = await client.subscribe(query); return _latestSubscription; diff --git a/lib/src/network/parse_live_query_web.dart b/lib/src/network/parse_live_query_web.dart deleted file mode 100644 index 6d9012785..000000000 --- a/lib/src/network/parse_live_query_web.dart +++ /dev/null @@ -1,462 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:html' as html; - -import 'package:connectivity/connectivity.dart'; -import 'package:flutter/widgets.dart'; - -import '../../parse_server_sdk.dart'; - -enum LiveQueryEvent { create, enter, update, leave, delete, error } - -const String _printConstLiveQuery = 'LiveQuery: '; - -// // ignore_for_file: always_specify_types -class Subscription { - Subscription(this.query, this.requestId, {T copyObject}) { - _copyObject = copyObject; - } - QueryBuilder query; - T _copyObject; - int requestId; - bool _enabled = false; - final List _liveQueryEvent = [ - 'create', - 'enter', - 'update', - 'leave', - 'delete', - 'error' - ]; - Map eventCallbacks = {}; - void on(LiveQueryEvent op, Function callback) { - eventCallbacks[_liveQueryEvent[op.index]] = callback; - } - - T get copyObject { - return _copyObject; - } -} - -enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } - -class LiveQueryReconnectingController with WidgetsBindingObserver { - LiveQueryReconnectingController( - this._reconnect, this._eventStream, this.debug) { - _connectivityChanged(ConnectivityResult.wifi); - _eventStream.listen((LiveQueryClientEvent event) { - switch (event) { - case LiveQueryClientEvent.CONNECTED: - _isConnected = true; - _retryState = 0; - _userDisconnected = false; - break; - case LiveQueryClientEvent.DISCONNECTED: - _isConnected = false; - _setReconnect(); - break; - case LiveQueryClientEvent.USER_DISCONNECTED: - _userDisconnected = true; - if (_currentTimer != null) { - _currentTimer.cancel(); - _currentTimer = null; - } - break; - } - - if (debug) { - print('$DEBUG_TAG: $event'); - } - }); - WidgetsBinding.instance.addObserver(this); - } - - static List get retryInterval => ParseCoreData().liveListRetryIntervals; - static const String DEBUG_TAG = 'LiveQueryReconnectingController'; - - final Function _reconnect; - final Stream _eventStream; - final bool debug; - - int _retryState = 0; - bool _isOnline = false; - bool _isConnected = false; - bool _userDisconnected = false; - - Timer _currentTimer; - - void _connectivityChanged(ConnectivityResult state) { - if (!_isOnline && state != ConnectivityResult.none) { - _retryState = 0; - } - _isOnline = state != ConnectivityResult.none; - if (debug) { - print('$DEBUG_TAG: $state'); - } - _setReconnect(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.resumed: - _setReconnect(); - break; - default: - break; - } - } - - void _setReconnect() { - if (_isOnline && - !_isConnected && - _currentTimer == null && - !_userDisconnected && - retryInterval[_retryState] >= 0) { - _currentTimer = - Timer(Duration(milliseconds: retryInterval[_retryState]), () { - _currentTimer = null; - _reconnect(); - }); - if (debug) - print('$DEBUG_TAG: Retrytimer set to ${retryInterval[_retryState]}ms'); - if (_retryState < retryInterval.length - 1) { - _retryState++; - } - } - } -} - -class Client { - factory Client() => _getInstance(); - Client._internal( - {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { - _clientEventStreamController = StreamController(); - _clientEventStream = - _clientEventStreamController.stream.asBroadcastStream(); - - _client = client ?? - ParseHTTPClient( - sendSessionId: - autoSendSessionId ?? ParseCoreData().autoSendSessionId, - securityContext: ParseCoreData().securityContext); - - _debug = isDebugEnabled(objectLevelDebug: debug); - _sendSessionId = - autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; - _liveQueryURL = _client.data.liveQueryURL; - if (_liveQueryURL.contains('https')) { - _liveQueryURL = _liveQueryURL.replaceAll('https', 'wss'); - } else if (_liveQueryURL.contains('http')) { - _liveQueryURL = _liveQueryURL.replaceAll('http', 'ws'); - } - - reconnectingController = LiveQueryReconnectingController( - () => reconnect(userInitialized: false), getClientEventStream, _debug); - } - static Client get instance => _getInstance(); - static Client _instance; - static Client _getInstance( - {bool debug, ParseHTTPClient client, bool autoSendSessionId}) { - _instance ??= Client._internal( - debug: debug, client: client, autoSendSessionId: autoSendSessionId); - return _instance; - } - - Stream get getClientEventStream { - return _clientEventStream; - } - - html.WebSocket _webSocket; - ParseHTTPClient _client; - bool _debug; - bool _sendSessionId; - Stream _stream; - String _liveQueryURL; - bool _connecting = false; - StreamController _clientEventStreamController; - Stream _clientEventStream; - LiveQueryReconnectingController reconnectingController; - - final Map _requestSubScription = {}; - - Future reconnect({bool userInitialized = false}) async { - await _connect(userInitialized: userInitialized); - _connectLiveQuery(); - } - - int readyState() { - if (_webSocket != null) { - return _webSocket.readyState; - } - return html.WebSocket.CONNECTING; - } - - Future disconnect({bool userInitialized = false}) async { - if (_webSocket != null && _webSocket.readyState == html.WebSocket.OPEN) { - if (_debug) { - print('$_printConstLiveQuery: Socket closed'); - } - _webSocket.close(); - _webSocket = null; - } - if (_webSocket != null) { - if (_debug) { - print('$_printConstLiveQuery: close'); - } - _webSocket.close(); - _webSocket = null; - _stream = null; - } - - _requestSubScription.values.toList().forEach((Subscription subscription) { - subscription._enabled = false; - }); - _connecting = false; - if (userInitialized) - _clientEventStreamController.sink - .add(LiveQueryClientEvent.USER_DISCONNECTED); - } - - Future> subscribe( - QueryBuilder query, - {T copyObject}) async { - if (_webSocket == null) { - await _clientEventStream.any((LiveQueryClientEvent event) => - event == LiveQueryClientEvent.CONNECTED); - } - final int requestId = _requestIdGenerator(); - final Subscription subscription = - Subscription(query, requestId, copyObject: copyObject); - _requestSubScription[requestId] = subscription; - //After a client connects to the LiveQuery server, - //it can send a subscribe message to subscribe a ParseQuery. - _subscribeLiveQuery(subscription); - return subscription; - } - - void unSubscribe(Subscription subscription) { - //Mount message for Unsubscribe - final Map unsubscribeMessage = { - 'op': 'unsubscribe', - 'requestId': subscription.requestId, - }; - if (_webSocket != null) { - if (_debug) { - print('$_printConstLiveQuery: UnsubscribeMessage: $unsubscribeMessage'); - } - _webSocket.send(jsonEncode(unsubscribeMessage)); -// _channel.sink.add(jsonEncode(unsubscribeMessage)); - subscription._enabled = false; - _requestSubScription.remove(subscription.requestId); - } - } - - static int _requestIdCount = 1; - - int _requestIdGenerator() { - return _requestIdCount++; - } - - Future _connect({bool userInitialized = false}) async { - if (_connecting) { - print('already connecting'); - return Future.value(null); - } - await disconnect(userInitialized: userInitialized); - _connecting = true; - - try { - _webSocket = html.WebSocket(_liveQueryURL); - await _webSocket.onOpen.first; - - _connecting = false; - if (_webSocket != null && _webSocket.readyState == html.WebSocket.OPEN) { - if (_debug) { - print('$_printConstLiveQuery: Socket opened'); - } - } else { - if (_debug) { - print('$_printConstLiveQuery: Error when connection client'); - } - return Future.value(null); - } - _stream = _webSocket.onMessage; - - _stream.listen((html.MessageEvent event) { - final dynamic message = event.data; - _handleMessage(message); - }, onDone: () { - _clientEventStreamController.sink - .add(LiveQueryClientEvent.DISCONNECTED); - if (_debug) { - print('$_printConstLiveQuery: Done'); - } - }, onError: (Object error) { - _clientEventStreamController.sink - .add(LiveQueryClientEvent.DISCONNECTED); - if (_debug) { - print( - '$_printConstLiveQuery: Error: ${error.runtimeType.toString()}'); - } - return Future.value(handleException( - Exception(error), ParseApiRQ.liveQuery, _debug, 'HtmlWebSocket')); - }); - } on Exception catch (e) { - _connecting = false; - _clientEventStreamController.sink.add(LiveQueryClientEvent.DISCONNECTED); - if (_debug) { - print('$_printConstLiveQuery: Error: ${e.toString()}'); - } - return handleException(e, ParseApiRQ.liveQuery, _debug, 'LiveQuery'); - } - } - - void _connectLiveQuery() { - if (_webSocket == null) { - return; - } - //The connect message is sent from a client to the LiveQuery server. - //It should be the first message sent from a client after the WebSocket connection is established. - final Map connectMessage = { - 'op': 'connect', - 'applicationId': _client.data.applicationId - }; - - if (_sendSessionId && _client.data.sessionId != null) { - connectMessage['sessionToken'] = _client.data.sessionId; - } - - if (_client.data.clientKey != null) - connectMessage['clientKey'] = _client.data.clientKey; - if (_client.data.masterKey != null) - connectMessage['masterKey'] = _client.data.masterKey; - - if (_debug) { - print('$_printConstLiveQuery: ConnectMessage: $connectMessage'); - } - _webSocket.send(jsonEncode(connectMessage)); -// _channel.sink.add(jsonEncode(connectMessage)); - } - - void _subscribeLiveQuery(Subscription subscription) { - if (subscription._enabled) { - return; - } - subscription._enabled = true; - - final QueryBuilder query = subscription.query; - final List keysToReturn = query.limiters['keys']?.split(','); - query.limiters.clear(); //Remove limits in LiveQuery - final String _where = query.buildQuery().replaceAll('where=', ''); - - //Convert where condition to Map - Map _whereMap = Map(); - if (_where != '') { - _whereMap = json.decode(_where); - } - - final Map subscribeMessage = { - 'op': 'subscribe', - 'requestId': subscription.requestId, - 'query': { - 'className': query.object.parseClassName, - 'where': _whereMap, - if (keysToReturn != null && keysToReturn.isNotEmpty) - 'fields': keysToReturn - } - }; - if (_sendSessionId && _client.data.sessionId != null) { - subscribeMessage['sessionToken'] = _client.data.sessionId; - } - - if (_debug) { - print('$_printConstLiveQuery: SubscribeMessage: $subscribeMessage'); - } - - _webSocket.send(jsonEncode(subscribeMessage)); -// _channel.sink.add(jsonEncode(subscribeMessage)); - } - - void _handleMessage(String message) { - if (_debug) { - print('$_printConstLiveQuery: Listen: $message'); - } - - final Map actionData = jsonDecode(message); - - Subscription subscription; - if (actionData.containsKey('op') && actionData['op'] == 'connected') { - print('ReSubScription:$_requestSubScription'); - - _requestSubScription.values.toList().forEach((Subscription subcription) { - _subscribeLiveQuery(subcription); - }); - _clientEventStreamController.sink.add(LiveQueryClientEvent.CONNECTED); - return; - } - if (actionData.containsKey('requestId')) { - subscription = _requestSubScription[actionData['requestId']]; - } - if (subscription == null) { - return; - } - if (subscription.eventCallbacks.containsKey(actionData['op'])) { - if (actionData.containsKey('object')) { - final Map map = actionData['object']; - final String className = map['className']; - if (className == keyClassUser) { - subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? - ParseCoreData.instance.createParseUser(null, null, null)) - .fromJson(map)); - } else { - subscription.eventCallbacks[actionData['op']]( - (subscription.copyObject ?? - ParseCoreData.instance.createObject(className)) - .fromJson(map)); - } - } else { - subscription.eventCallbacks[actionData['op']](actionData); - } - } - } -} - -class LiveQuery { - LiveQuery({bool debug, ParseHTTPClient client, bool autoSendSessionId}) { - _client = client ?? - ParseHTTPClient( - sendSessionId: - autoSendSessionId ?? ParseCoreData().autoSendSessionId, - securityContext: ParseCoreData().securityContext); - - _debug = isDebugEnabled(objectLevelDebug: debug); - _sendSessionId = - autoSendSessionId ?? ParseCoreData().autoSendSessionId ?? true; - this.client = Client._getInstance( - client: _client, debug: _debug, autoSendSessionId: _sendSessionId); - } - - ParseHTTPClient _client; - bool _debug; - bool _sendSessionId; - Subscription _latestSubscription; - Client client; - - @deprecated - Future subscribe(QueryBuilder query) async { - _latestSubscription = await client.subscribe(query); - return _latestSubscription; - } - - @deprecated - Future unSubscribe() async { - client.unSubscribe(_latestSubscription); - } - - @deprecated - void on(LiveQueryEvent op, Function callback) { - _latestSubscription.on(op, callback); - } -} diff --git a/lib/src/network/parse_websocket.dart b/lib/src/network/parse_websocket.dart new file mode 100644 index 000000000..4ec07c900 --- /dev/null +++ b/lib/src/network/parse_websocket.dart @@ -0,0 +1,2 @@ +export 'parse_websocket_io.dart' + if (dart.library.js) 'parse_websocket_html.dart'; diff --git a/lib/src/network/parse_websocket_html.dart b/lib/src/network/parse_websocket_html.dart new file mode 100644 index 000000000..aeab1d27c --- /dev/null +++ b/lib/src/network/parse_websocket_html.dart @@ -0,0 +1,33 @@ +/// If you change this file, you should apply the same changes to the 'parse_websocket_io.dart' file + +import 'dart:html' as html; + +import 'package:web_socket_channel/html.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class WebSocket { + WebSocket._(this._webSocket); + + static const int CONNECTING = 0; + static const int OPEN = 1; + static const int CLOSING = 2; + static const int CLOSED = 3; + + final html.WebSocket _webSocket; + + static Future connect(String liveQueryURL) async { + final html.WebSocket webSocket = html.WebSocket(liveQueryURL); + await webSocket.onOpen.first; + return WebSocket._(webSocket); + } + + int get readyState => _webSocket.readyState; + + Future close() async { + return _webSocket.close(); + } + + WebSocketChannel createWebSocketChannel() { + return HtmlWebSocketChannel(_webSocket); + } +} diff --git a/lib/src/network/parse_websocket_io.dart b/lib/src/network/parse_websocket_io.dart new file mode 100644 index 000000000..37206dd49 --- /dev/null +++ b/lib/src/network/parse_websocket_io.dart @@ -0,0 +1,31 @@ +/// If you change this file, you should apply the same changes to the 'parse_websocket_html.dart' file + +import 'dart:io' as io; + +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class WebSocket { + WebSocket._(this._webSocket); + + static const int CONNECTING = 0; + static const int OPEN = 1; + static const int CLOSING = 2; + static const int CLOSED = 3; + + final io.WebSocket _webSocket; + + static Future connect(String liveQueryURL) async { + return WebSocket._(await io.WebSocket.connect(liveQueryURL)); + } + + int get readyState => _webSocket.readyState; + + Future close() { + return _webSocket.close(); + } + + WebSocketChannel createWebSocketChannel() { + return IOWebSocketChannel(_webSocket); + } +} From a0b60204bfd3e495fcd969d598da76fe89b2435f Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 4 Sep 2020 06:11:44 +0200 Subject: [PATCH 04/33] Seperate the dart and the flutter parts of this sdk (#434) * Set TODO's Set TODO's, needed for https://github.com/parse-community/Parse-SDK-Flutter/issues/78#issuecomment-682154173 * First step of seperating the flutter and the dart parts * added one todo * remove connectivity from the dart part * Removed path_provider from the dart part Not so happy with the fact that `CoreStoreSembastImp implements sdk.CoreStoreSembastImp` in `parse_server_sdk.dart` but I didn't find a better solution. * Removed shared_preferences from the dart part * Update pubspec.yaml * Update parse_server_sdk_dart.dart * removed dart:ui from dart part * removed flutter from dart part * fixed tests --- example/pubspec.yaml | 2 + lib/parse_server_sdk.dart | 248 +++++++++++--------- lib/parse_server_sdk_dart.dart | 161 +++++++++++++ lib/src/data/parse_core_data.dart | 38 +++- lib/src/network/parse_connectivity.dart | 18 ++ lib/src/network/parse_live_query.dart | 38 ++-- lib/src/objects/parse_file.dart | 7 +- lib/src/objects/parse_installation.dart | 16 +- lib/src/storage/core_store_sem_impl.dart | 7 +- lib/src/storage/core_store_sp_impl.dart | 3 +- lib/src/utils/parse_live_list.dart | 253 +-------------------- lib/src/utils/parse_live_list_flutter.dart | 245 ++++++++++++++++++++ pubspec.yaml | 8 +- test/parse_client_configuration_test.dart | 6 + test/parse_query_test.dart | 18 +- 15 files changed, 663 insertions(+), 405 deletions(-) create mode 100644 lib/parse_server_sdk_dart.dart create mode 100644 lib/src/network/parse_connectivity.dart create mode 100644 lib/src/utils/parse_live_list_flutter.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 620f1db5f..889345e65 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sembast: ^2.0.1 shared_preferences: ^0.5.0 + path_provider: ^1.6.14 + dev_dependencies: parse_server_sdk: path: ../ diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index 681143501..b84c7a83b 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -1,156 +1,198 @@ -library flutter_parse_sdk; - import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:connectivity/connectivity.dart'; import 'package:flutter/widgets.dart'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; -import 'package:meta/meta.dart'; import 'package:package_info/package_info.dart'; -import 'package:parse_server_sdk/src/network/parse_websocket.dart' - as parse_web_socket; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:sembast/sembast.dart'; -import 'package:sembast/sembast_io.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:uuid/uuid.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import 'package:xxtea/xxtea.dart'; - -export 'src/utils/parse_live_list.dart'; - -part 'package:parse_server_sdk/src/data/core_store.dart'; -part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; -part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; -part 'package:parse_server_sdk/src/storage/core_store_sp_impl.dart'; -part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; -part 'src/base/parse_constants.dart'; -part 'src/data/parse_core_data.dart'; -part 'src/enums/parse_enum_api_rq.dart'; -part 'src/network/parse_http_client.dart'; -part 'src/network/parse_live_query.dart'; -part 'src/network/parse_query.dart'; -part 'src/objects/parse_acl.dart'; -part 'src/objects/parse_base.dart'; -part 'src/objects/parse_cloneable.dart'; -part 'src/objects/parse_config.dart'; -part 'src/objects/parse_error.dart'; -part 'src/objects/parse_file.dart'; -part 'src/objects/parse_file_base.dart'; -part 'src/objects/parse_file_web.dart'; -part 'src/objects/parse_function.dart'; -part 'src/objects/parse_geo_point.dart'; -part 'src/objects/parse_installation.dart'; -part 'src/objects/parse_merge.dart'; -part 'src/objects/parse_object.dart'; -part 'src/objects/parse_relation.dart'; -part 'src/objects/parse_response.dart'; -part 'src/objects/parse_session.dart'; -part 'src/objects/parse_user.dart'; -part 'src/utils/parse_date_format.dart'; -part 'src/utils/parse_decoder.dart'; -part 'src/utils/parse_encoder.dart'; -part 'src/utils/parse_file_extensions.dart'; -part 'src/utils/parse_logger.dart'; -part 'src/utils/parse_login_helpers.dart'; -part 'src/utils/parse_utils.dart'; - -class Parse { - ParseCoreData data; - bool _hasBeenInitialized = false; +import 'parse_server_sdk_dart.dart' as sdk; +import 'src/storage/core_store_sp_impl.dart'; + +export 'parse_server_sdk_dart.dart' hide Parse, CoreStoreSembastImp; +export 'src/storage/core_store_sp_impl.dart'; +export 'src/utils/parse_live_list_flutter.dart'; + +class Parse extends sdk.Parse + with WidgetsBindingObserver + implements sdk.ParseConnectivityProvider { /// To initialize Parse Server in your application /// /// This should be initialized in MyApp() creation /// /// ``` /// Parse().initialize( - // "PARSE_APP_ID", - // "https://parse.myaddress.com/parse/, - // masterKey: "asd23rjh234r234r234r", - // debug: true, - // liveQuery: true); - // ``` + /// "PARSE_APP_ID", + /// "https://parse.myaddress.com/parse/, + /// masterKey: "asd23rjh234r234r234r", + /// debug: true, + /// liveQuery: true); + /// ``` + /// [appName], [appVersion] and [appPackageName] are automatically set on Android and IOS, if they are not defined. You should provide a value on web. + /// [fileDirectory] is not used on web + @override Future initialize( String appId, String serverUrl, { bool debug = false, - String appName = '', + String appName, + String appVersion, + String appPackageName, + String locale, String liveQueryUrl, String clientKey, String masterKey, String sessionId, bool autoSendSessionId, SecurityContext securityContext, - CoreStore coreStore, - Map registeredSubClassMap, - ParseUserConstructor parseUserConstructor, - ParseFileConstructor parseFileConstructor, + sdk.CoreStore coreStore, + Map registeredSubClassMap, + sdk.ParseUserConstructor parseUserConstructor, + sdk.ParseFileConstructor parseFileConstructor, List liveListRetryIntervals, + sdk.ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, }) async { - final String url = removeTrailingSlash(serverUrl); + if (!sdk.parseIsWeb && (appName == null || appVersion == null || appPackageName == null)) { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appName ??= packageInfo.appName; + appVersion ??= packageInfo.version; + appPackageName ??= packageInfo.packageName; + } - await ParseCoreData.init( + return await super.initialize( appId, - url, + serverUrl, debug: debug, appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale ?? sdk.parseIsWeb + ? ui.window.locale.toString() + : Platform.localeName, liveQueryUrl: liveQueryUrl, - masterKey: masterKey, clientKey: clientKey, + masterKey: masterKey, sessionId: sessionId, autoSendSessionId: autoSendSessionId, securityContext: securityContext, - store: coreStore, + coreStore: coreStore ?? + await CoreStoreSharedPrefsImp.getInstance(password: masterKey), registeredSubClassMap: registeredSubClassMap, parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, - liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider ?? this, + fileDirectory: fileDirectory ?? (await getTemporaryDirectory()).path, + appResumedStream: appResumedStream ?? _appResumedStreamController.stream, ); + } - _hasBeenInitialized = true; + final StreamController _appResumedStreamController = + StreamController(); + + @override + Future checkConnectivity() async { + //Connectivity works differently on web + if (!sdk.parseIsWeb) { + switch (await Connectivity().checkConnectivity()) { + case ConnectivityResult.wifi: + return sdk.ParseConnectivityResult.wifi; + case ConnectivityResult.mobile: + return sdk.ParseConnectivityResult.mobile; + case ConnectivityResult.none: + return sdk.ParseConnectivityResult.none; + } + } + return sdk.ParseConnectivityResult.wifi; + } - return this; + @override + Stream get connectivityStream { + return Connectivity().onConnectivityChanged.map((ConnectivityResult event) { + switch (event) { + case ConnectivityResult.wifi: + return sdk.ParseConnectivityResult.wifi; + case ConnectivityResult.mobile: + return sdk.ParseConnectivityResult.mobile; + default: + return sdk.ParseConnectivityResult.none; + } + }); } - bool hasParseBeenInitialized() => _hasBeenInitialized; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _appResumedStreamController.sink.add(null); + } +} - Future healthCheck( - {bool debug, ParseHTTPClient client, bool sendSessionIdByDefault}) async { - ParseResponse parseResponse; +class CoreStoreSembastImp implements sdk.CoreStoreSembastImp { + CoreStoreSembastImp._(); + + static sdk.CoreStoreSembastImp _sembastImp; + + static Future getInstance(String dbPath, + {DatabaseFactory factory, String password}) async { + if (_sembastImp == null) { + String dbDirectory = ''; + if (!sdk.parseIsWeb && + (Platform.isIOS || Platform.isAndroid || Platform.isMacOS)) + dbDirectory = (await getApplicationDocumentsDirectory()).path; + final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); + _sembastImp ??= await sdk.CoreStoreSembastImp.getInstance(dbPath, + factory: factory, password: password); + } + return CoreStoreSembastImp._(); + } - final bool _debug = isDebugEnabled(objectLevelDebug: debug); + @override + Future clear() => _sembastImp.clear(); - final ParseHTTPClient _client = client ?? - ParseHTTPClient( - sendSessionId: - sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, - securityContext: ParseCoreData().securityContext); + @override + Future containsKey(String key) => _sembastImp.containsKey(key); - const String className = 'parseBase'; - const ParseApiRQ type = ParseApiRQ.healthCheck; + @override + Future get(String key) => _sembastImp.get(key); - try { - final Response response = - await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); - parseResponse = - handleResponse(null, response, type, _debug, className); - } on Exception catch (e) { - parseResponse = handleException(e, type, _debug, className); - } + @override + Future getBool(String key) => _sembastImp.getBool(key); - return parseResponse; - } + @override + Future getDouble(String key) => _sembastImp.getDouble(key); + + @override + Future getInt(String key) => _sembastImp.getInt(key); + + @override + Future getString(String key) => _sembastImp.getString(key); + + @override + Future> getStringList(String key) => + _sembastImp.getStringList(key); + + @override + Future remove(String key) => _sembastImp.remove(key); + + @override + Future setBool(String key, bool value) => + _sembastImp.setBool(key, value); + + @override + Future setDouble(String key, double value) => + _sembastImp.setDouble(key, value); + + @override + Future setInt(String key, int value) => _sembastImp.setInt(key, value); + + @override + Future setString(String key, String value) => + _sembastImp.setString(key, value); + + @override + Future setStringList(String key, List values) => + _sembastImp.setStringList(key, values); } diff --git a/lib/parse_server_sdk_dart.dart b/lib/parse_server_sdk_dart.dart new file mode 100644 index 000000000..1da9ae8ae --- /dev/null +++ b/lib/parse_server_sdk_dart.dart @@ -0,0 +1,161 @@ +library flutter_parse_sdk; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:meta/meta.dart'; +import 'package:parse_server_sdk/src/network/parse_websocket.dart' + as parse_web_socket; +import 'package:path/path.dart' as path; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:xxtea/xxtea.dart'; + +part 'package:parse_server_sdk/src/data/core_store.dart'; +part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; +part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; +part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; +part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; +part 'src/base/parse_constants.dart'; +part 'src/data/parse_core_data.dart'; +part 'src/enums/parse_enum_api_rq.dart'; +part 'src/network/parse_connectivity.dart'; +part 'src/network/parse_http_client.dart'; +part 'src/network/parse_live_query.dart'; +part 'src/network/parse_query.dart'; +part 'src/objects/parse_acl.dart'; +part 'src/objects/parse_base.dart'; +part 'src/objects/parse_cloneable.dart'; +part 'src/objects/parse_config.dart'; +part 'src/objects/parse_error.dart'; +part 'src/objects/parse_file.dart'; +part 'src/objects/parse_file_base.dart'; +part 'src/objects/parse_file_web.dart'; +part 'src/objects/parse_function.dart'; +part 'src/objects/parse_geo_point.dart'; +part 'src/objects/parse_installation.dart'; +part 'src/objects/parse_merge.dart'; +part 'src/objects/parse_object.dart'; +part 'src/objects/parse_relation.dart'; +part 'src/objects/parse_response.dart'; +part 'src/objects/parse_session.dart'; +part 'src/objects/parse_user.dart'; +part 'src/utils/parse_date_format.dart'; +part 'src/utils/parse_decoder.dart'; +part 'src/utils/parse_encoder.dart'; +part 'src/utils/parse_file_extensions.dart'; +part 'src/utils/parse_logger.dart'; +part 'src/utils/parse_login_helpers.dart'; +part 'src/utils/parse_utils.dart'; +part 'src/utils/parse_live_list.dart'; + +class Parse { + ParseCoreData data; + bool _hasBeenInitialized = false; + + /// To initialize Parse Server in your application + /// + /// This should be initialized in MyApp() creation + /// + /// ``` + /// Parse().initialize( + /// "PARSE_APP_ID", + /// "https://parse.myaddress.com/parse/, + /// masterKey: "asd23rjh234r234r234r", + /// debug: true, + /// liveQuery: true); + /// ``` + Future initialize( + String appId, + String serverUrl, { + bool debug = false, + String appName, + String appVersion, + String appPackageName, + String locale, + String liveQueryUrl, + String clientKey, + String masterKey, + String sessionId, + bool autoSendSessionId, + SecurityContext securityContext, + CoreStore coreStore, + Map registeredSubClassMap, + ParseUserConstructor parseUserConstructor, + ParseFileConstructor parseFileConstructor, + List liveListRetryIntervals, + ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, + }) async { + final String url = removeTrailingSlash(serverUrl); + + await ParseCoreData.init( + appId, + url, + debug: debug, + appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale, + liveQueryUrl: liveQueryUrl, + masterKey: masterKey, + clientKey: clientKey, + sessionId: sessionId, + autoSendSessionId: autoSendSessionId, + securityContext: securityContext, + store: coreStore, + registeredSubClassMap: registeredSubClassMap, + parseUserConstructor: parseUserConstructor, + parseFileConstructor: parseFileConstructor, + liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider, + fileDirectory: fileDirectory, + appResumedStream: appResumedStream, + ); + + _hasBeenInitialized = true; + + return this; + } + + bool hasParseBeenInitialized() => _hasBeenInitialized; + + Future healthCheck( + {bool debug, ParseHTTPClient client, bool sendSessionIdByDefault}) async { + ParseResponse parseResponse; + + final bool _debug = isDebugEnabled(objectLevelDebug: debug); + + final ParseHTTPClient _client = client ?? + ParseHTTPClient( + sendSessionId: + sendSessionIdByDefault ?? ParseCoreData().autoSendSessionId, + securityContext: ParseCoreData().securityContext); + + const String className = 'parseBase'; + const ParseApiRQ type = ParseApiRQ.healthCheck; + + try { + final Response response = + await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); + parseResponse = + handleResponse(null, response, type, _debug, className); + } on Exception catch (e) { + parseResponse = handleException(e, type, _debug, className); + } + + return parseResponse; + } +} diff --git a/lib/src/data/parse_core_data.dart b/lib/src/data/parse_core_data.dart index 02b37c2af..514b83dd5 100644 --- a/lib/src/data/parse_core_data.dart +++ b/lib/src/data/parse_core_data.dart @@ -19,6 +19,9 @@ class ParseCoreData { String serverUrl, { bool debug, String appName, + String appVersion, + String appPackageName, + String locale, String liveQueryUrl, String masterKey, String clientKey, @@ -30,11 +33,16 @@ class ParseCoreData { ParseUserConstructor parseUserConstructor, ParseFileConstructor parseFileConstructor, List liveListRetryIntervals, + ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, }) async { _instance = ParseCoreData._init(appId, serverUrl); - _instance.storage ??= - store ?? await CoreStoreSharedPrefsImp.getInstance(password: masterKey); + assert(_instance.storage != null || store != null, + 'There is no CoreStore set.'); + + _instance.storage ??= store; if (debug != null) { _instance.debug = debug; @@ -42,6 +50,15 @@ class ParseCoreData { if (appName != null) { _instance.appName = appName; } + if (appVersion != null) { + _instance.appVersion = appVersion; + } + if (appPackageName != null) { + _instance.appPackageName = appPackageName; + } + if (locale != null) { + _instance.locale = locale; + } if (liveQueryUrl != null) { _instance.liveQueryURL = liveQueryUrl; } @@ -73,10 +90,24 @@ class ParseCoreData { parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, ); + if (connectivityProvider != null) { + _instance.connectivityProvider = connectivityProvider; + } + + if (fileDirectory != null) { + _instance.fileDirectory = fileDirectory; + } + + if(appResumedStream!= null){ + _instance.appResumedStream = appResumedStream; + } } String appName; + String appVersion; + String appPackageName; String applicationId; + String locale; String serverUrl; String liveQueryURL; String masterKey; @@ -88,6 +119,9 @@ class ParseCoreData { CoreStore storage; ParseSubClassHandler _subClassHandler; List liveListRetryIntervals; + ParseConnectivityProvider connectivityProvider; + String fileDirectory; + Stream appResumedStream; void registerSubClass( String className, ParseObjectConstructor objectConstructor) { diff --git a/lib/src/network/parse_connectivity.dart b/lib/src/network/parse_connectivity.dart new file mode 100644 index 000000000..731ad1a4b --- /dev/null +++ b/lib/src/network/parse_connectivity.dart @@ -0,0 +1,18 @@ +part of flutter_parse_sdk; + +/// Connection status check result. +enum ParseConnectivityResult { + /// WiFi: Device connected via Wi-Fi + wifi, + + /// Mobile: Device connected to cellular network + mobile, + + /// None: Device not connected to any network + none +} + +abstract class ParseConnectivityProvider { + Future checkConnectivity(); + Stream get connectivityStream; +} diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index 05e6be9a4..cd2b861be 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -33,15 +33,20 @@ class Subscription { enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } -class LiveQueryReconnectingController with WidgetsBindingObserver { +class LiveQueryReconnectingController { LiveQueryReconnectingController( - this._reconnect, this._eventStream, this.debug) { - //Connectivity works differently on web - if (!parseIsWeb) { - Connectivity().checkConnectivity().then(_connectivityChanged); - Connectivity().onConnectivityChanged.listen(_connectivityChanged); + this._reconnect, + this._eventStream, + this.debug, + ) { + final ParseConnectivityProvider connectivityProvider = + ParseCoreData().connectivityProvider; + if (connectivityProvider != null) { + connectivityProvider.checkConnectivity().then(_connectivityChanged); + connectivityProvider.connectivityStream.listen(_connectivityChanged); } else { - _connectivityChanged(ConnectivityResult.wifi); + print( + 'LiveQuery does not work, if there is ParseConnectivityProvider provided.'); } _eventStream.listen((LiveQueryClientEvent event) { switch (event) { @@ -67,7 +72,7 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { print('$DEBUG_TAG: $event'); } }); - WidgetsBinding.instance.addObserver(this); + ParseCoreData().appResumedStream?.listen((void _) => _setReconnect()); } static List get retryInterval => ParseCoreData().liveListRetryIntervals; @@ -84,28 +89,17 @@ class LiveQueryReconnectingController with WidgetsBindingObserver { Timer _currentTimer; - void _connectivityChanged(ConnectivityResult state) { - if (!_isOnline && state != ConnectivityResult.none) { + void _connectivityChanged(ParseConnectivityResult state) { + if (!_isOnline && state != ParseConnectivityResult.none) { _retryState = 0; } - _isOnline = state != ConnectivityResult.none; + _isOnline = state != ParseConnectivityResult.none; if (debug) { print('$DEBUG_TAG: $state'); } _setReconnect(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.resumed: - _setReconnect(); - break; - default: - break; - } - } - void _setReconnect() { if (_isOnline && !_isConnected && diff --git a/lib/src/objects/parse_file.dart b/lib/src/objects/parse_file.dart index c44d63ab3..a149839df 100644 --- a/lib/src/objects/parse_file.dart +++ b/lib/src/objects/parse_file.dart @@ -21,14 +21,12 @@ class ParseFile extends ParseFileBase { File file; Future loadStorage() async { - final Directory tempPath = await getTemporaryDirectory(); - if (name == null) { file = null; return this; } - final File possibleFile = File('${tempPath.path}/$name'); + final File possibleFile = File('${ParseCoreData().fileDirectory}/$name'); // ignore: avoid_slow_async_io final bool exists = await possibleFile.exists(); @@ -47,8 +45,7 @@ class ParseFile extends ParseFileBase { return this; } - final Directory tempPath = await getTemporaryDirectory(); - file = File('${tempPath.path}/$name'); + file = File('${ParseCoreData().fileDirectory}/$name'); await file.create(); final Response response = await _client.get(url); await file.writeAsBytes(response.bodyBytes); diff --git a/lib/src/objects/parse_installation.dart b/lib/src/objects/parse_installation.dart index ad44b3470..c30937742 100644 --- a/lib/src/objects/parse_installation.dart +++ b/lib/src/objects/parse_installation.dart @@ -83,22 +83,14 @@ class ParseInstallation extends ParseObject { } //Locale - final String locale = parseIsWeb - ? ui.window.locale.toString() - : Platform.localeName; - if (locale != null && locale.isNotEmpty) { - set(keyLocaleIdentifier, locale); - } + set(keyLocaleIdentifier, ParseCoreData().locale); //Timezone //App info - if (!parseIsWeb) { - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - set(keyAppName, packageInfo.appName); - set(keyAppVersion, packageInfo.version); - set(keyAppIdentifier, packageInfo.packageName); - } + set(keyAppName, ParseCoreData().appName); + set(keyAppVersion, ParseCoreData().appVersion); + set(keyAppIdentifier, ParseCoreData().appPackageName); set(keyParseVersion, keySdkVersion); } diff --git a/lib/src/storage/core_store_sem_impl.dart b/lib/src/storage/core_store_sem_impl.dart index f8e2c317d..bf60602bd 100644 --- a/lib/src/storage/core_store_sem_impl.dart +++ b/lib/src/storage/core_store_sem_impl.dart @@ -6,16 +6,11 @@ class CoreStoreSembastImp implements CoreStore { static CoreStoreSembastImp _instance; - static Future getInstance( + static Future getInstance(String dbPath, {DatabaseFactory factory, String password = 'flutter_sdk'}) async { if (_instance == null) { factory ??= databaseFactoryIo; final SembastCodec codec = getXXTeaSembastCodec(password: password); - String dbDirectory = ''; - if (!parseIsWeb && - (Platform.isIOS || Platform.isAndroid || Platform.isMacOS)) - dbDirectory = (await getApplicationDocumentsDirectory()).path; - final String dbPath = path.join('$dbDirectory/parse', 'parse.db'); final Database db = await factory.openDatabase(dbPath, codec: codec); _instance = CoreStoreSembastImp._internal(db, StoreRef.main()); diff --git a/lib/src/storage/core_store_sp_impl.dart b/lib/src/storage/core_store_sp_impl.dart index c09e6970a..e41693c43 100644 --- a/lib/src/storage/core_store_sp_impl.dart +++ b/lib/src/storage/core_store_sp_impl.dart @@ -1,4 +1,5 @@ -part of flutter_parse_sdk; +import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class CoreStoreSharedPrefsImp implements CoreStore { CoreStoreSharedPrefsImp._internal(this._store); diff --git a/lib/src/utils/parse_live_list.dart b/lib/src/utils/parse_live_list.dart index 0528e676d..827e27f6b 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/lib/src/utils/parse_live_list.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import '../../parse_server_sdk.dart'; +part of flutter_parse_sdk; // ignore_for_file: invalid_use_of_protected_member class ParseLiveList { @@ -335,8 +331,8 @@ class ParseLiveList { includeList.add(key); // ignore: avoid_as if ((includes[key] as Map).isNotEmpty) { - includeList - .addAll(_toIncludeStringList(includes[key]).map((String e) => '$key.$e')); + includeList.addAll( + _toIncludeStringList(includes[key]).map((String e) => '$key.$e')); } } return includeList; @@ -678,8 +674,8 @@ class ParseLiveListElement { ..keysToReturn([keyVarUpdatedAt]) ..whereEqualTo(keyVarObjectId, subObject.objectId); final ParseResponse parseResponse = await queryBuilder.query(); - if (parseResponse.success && parseResponse.results.first.updatedAt != - subObject.updatedAt) { + if (parseResponse.success && + parseResponse.results.first.updatedAt != subObject.updatedAt) { queryBuilder.limiters.remove('keys'); queryBuilder.includeObject(_getIncludeList(path[key])); final ParseResponse parseResponse = await queryBuilder.query(); @@ -735,8 +731,7 @@ class ParseLiveListDeleteEvent typedef StreamGetter = Stream Function(); typedef DataGetter = T Function(); -typedef ChildBuilder = Widget Function( - BuildContext context, ParseLiveListElementSnapshot snapshot); + class ParseLiveListElementSnapshot { ParseLiveListElementSnapshot({this.loadedData, this.error}); @@ -749,240 +744,4 @@ class ParseLiveListElementSnapshot { bool get failed => error != null; } -class ParseLiveListWidget extends StatefulWidget { - const ParseLiveListWidget({ - Key key, - @required this.query, - this.listLoadingElement, - this.duration = const Duration(milliseconds: 300), - this.scrollPhysics, - this.scrollController, - this.scrollDirection = Axis.vertical, - this.padding, - this.primary, - this.reverse = false, - this.childBuilder, - this.shrinkWrap = false, - this.removedItemBuilder, - this.listenOnAllSubItems, - this.listeningIncludes, - this.lazyLoading = true, - }) : super(key: key); - - final QueryBuilder query; - final Widget listLoadingElement; - final Duration duration; - final ScrollPhysics scrollPhysics; - final ScrollController scrollController; - - final Axis scrollDirection; - final EdgeInsetsGeometry padding; - final bool primary; - final bool reverse; - final bool shrinkWrap; - - final ChildBuilder childBuilder; - final ChildBuilder removedItemBuilder; - - final bool listenOnAllSubItems; - final List listeningIncludes; - - final bool lazyLoading; - - @override - _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( - query: query, - removedItemBuilder: removedItemBuilder, - listenOnAllSubItems: listenOnAllSubItems, - listeningIncludes: listeningIncludes, - lazyLoading: lazyLoading, - ); - - static Widget defaultChildBuilder( - BuildContext context, ParseLiveListElementSnapshot snapshot) { - Widget child; - if (snapshot.failed) { - child = const Text('something went wrong!'); - } else if (snapshot.hasData) { - child = ListTile( - title: Text( - snapshot.loadedData.get(keyVarObjectId), - ), - ); - } else { - child = const ListTile( - leading: CircularProgressIndicator(), - ); - } - return child; - } -} - -class _ParseLiveListWidgetState - extends State> { - _ParseLiveListWidgetState( - {@required this.query, - @required this.removedItemBuilder, - bool listenOnAllSubItems, - List listeningIncludes, - bool lazyLoading = true}) { - ParseLiveList.create( - query, - listenOnAllSubItems: listenOnAllSubItems, - listeningIncludes: listeningIncludes, - lazyLoading: lazyLoading, - ).then((ParseLiveList value) { - setState(() { - _liveList = value; - _liveList.stream.listen((ParseLiveListEvent event) { - if (event is ParseLiveListAddEvent) { - if (_animatedListKey.currentState != null) - _animatedListKey.currentState - .insertItem(event.index, duration: widget.duration); - } else if (event is ParseLiveListDeleteEvent) { - _animatedListKey.currentState.removeItem( - event.index, - (BuildContext context, Animation animation) => - ParseLiveListElementWidget( - key: ValueKey(event.object?.get( - keyVarObjectId, - defaultValue: 'removingItem')), - childBuilder: widget.childBuilder ?? - ParseLiveListWidget.defaultChildBuilder, - sizeFactor: animation, - duration: widget.duration, - loadedData: () => event.object, - ), - duration: widget.duration); - } - }); - }); - }); - } - - final QueryBuilder query; - ParseLiveList _liveList; - final GlobalKey _animatedListKey = - GlobalKey(); - final ChildBuilder removedItemBuilder; - @override - Widget build(BuildContext context) { - return _liveList == null - ? widget.listLoadingElement ?? Container() - : buildAnimatedList(); - } - - Widget buildAnimatedList() { - return AnimatedList( - key: _animatedListKey, - physics: widget.scrollPhysics, - controller: widget.scrollController, - scrollDirection: widget.scrollDirection, - padding: widget.padding, - primary: widget.primary, - reverse: widget.reverse, - shrinkWrap: widget.shrinkWrap, - initialItemCount: _liveList?.size, - itemBuilder: - (BuildContext context, int index, Animation animation) { - return ParseLiveListElementWidget( - key: ValueKey( - _liveList?.getIdentifier(index) ?? '_NotFound'), - stream: () => _liveList?.getAt(index), - loadedData: () => _liveList?.getLoadedAt(index), - sizeFactor: animation, - duration: widget.duration, - childBuilder: - widget.childBuilder ?? ParseLiveListWidget.defaultChildBuilder, - ); - }); - } - - @override - void dispose() { - _liveList?.dispose(); - _liveList = null; - super.dispose(); - } -} - -class ParseLiveListElementWidget extends StatefulWidget { - const ParseLiveListElementWidget( - {Key key, - this.stream, - this.loadedData, - @required this.sizeFactor, - @required this.duration, - @required this.childBuilder}) - : super(key: key); - - final StreamGetter stream; - final DataGetter loadedData; - final Animation sizeFactor; - final Duration duration; - final ChildBuilder childBuilder; - - @override - _ParseLiveListElementWidgetState createState() { - return _ParseLiveListElementWidgetState(loadedData, stream); - } -} - -class _ParseLiveListElementWidgetState - extends State> - with SingleTickerProviderStateMixin { - _ParseLiveListElementWidgetState( - DataGetter loadedDataGetter, StreamGetter stream) { - _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); - if (stream != null) { - _streamSubscription = stream().listen( - (T data) { - if (widget != null) { - setState(() { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); - }); - } else { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); - } - }, - onError: (Object error) { - if (error is ParseError) { - if (widget != null) { - setState(() { - _snapshot = ParseLiveListElementSnapshot(error: error); - }); - } else { - _snapshot = ParseLiveListElementSnapshot(error: error); - } - } - }, - cancelOnError: false, - ); - } - } - - ParseLiveListElementSnapshot _snapshot; - - StreamSubscription _streamSubscription; - - @override - void dispose() { - _streamSubscription?.cancel(); - _streamSubscription = null; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Widget result = SizeTransition( - sizeFactor: widget.sizeFactor, - child: AnimatedSize( - duration: widget.duration, - vsync: this, - child: widget.childBuilder(context, _snapshot), - ), - ); - return result; - } -} diff --git a/lib/src/utils/parse_live_list_flutter.dart b/lib/src/utils/parse_live_list_flutter.dart new file mode 100644 index 000000000..1d6eb394a --- /dev/null +++ b/lib/src/utils/parse_live_list_flutter.dart @@ -0,0 +1,245 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; + +typedef ChildBuilder = Widget Function( + BuildContext context, ParseLiveListElementSnapshot snapshot); + +class ParseLiveListWidget extends StatefulWidget { + const ParseLiveListWidget({ + Key key, + @required this.query, + this.listLoadingElement, + this.duration = const Duration(milliseconds: 300), + this.scrollPhysics, + this.scrollController, + this.scrollDirection = Axis.vertical, + this.padding, + this.primary, + this.reverse = false, + this.childBuilder, + this.shrinkWrap = false, + this.removedItemBuilder, + this.listenOnAllSubItems, + this.listeningIncludes, + this.lazyLoading = true, + }) : super(key: key); + + final QueryBuilder query; + final Widget listLoadingElement; + final Duration duration; + final ScrollPhysics scrollPhysics; + final ScrollController scrollController; + + final Axis scrollDirection; + final EdgeInsetsGeometry padding; + final bool primary; + final bool reverse; + final bool shrinkWrap; + + final ChildBuilder childBuilder; + final ChildBuilder removedItemBuilder; + + final bool listenOnAllSubItems; + final List listeningIncludes; + + final bool lazyLoading; + + @override + _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( + query: query, + removedItemBuilder: removedItemBuilder, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ); + + static Widget defaultChildBuilder( + BuildContext context, ParseLiveListElementSnapshot snapshot) { + Widget child; + if (snapshot.failed) { + child = const Text('something went wrong!'); + } else if (snapshot.hasData) { + child = ListTile( + title: Text( + snapshot.loadedData.get(keyVarObjectId), + ), + ); + } else { + child = const ListTile( + leading: CircularProgressIndicator(), + ); + } + return child; + } +} + +class _ParseLiveListWidgetState + extends State> { + _ParseLiveListWidgetState( + {@required this.query, + @required this.removedItemBuilder, + bool listenOnAllSubItems, + List listeningIncludes, + bool lazyLoading = true}) { + ParseLiveList.create( + query, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + ).then((ParseLiveList value) { + setState(() { + _liveList = value; + _liveList.stream.listen((ParseLiveListEvent event) { + if (event is ParseLiveListAddEvent) { + if (_animatedListKey.currentState != null) + _animatedListKey.currentState + .insertItem(event.index, duration: widget.duration); + } else if (event is ParseLiveListDeleteEvent) { + _animatedListKey.currentState.removeItem( + event.index, + (BuildContext context, Animation animation) => + ParseLiveListElementWidget( + key: ValueKey(event.object?.get( + keyVarObjectId, + defaultValue: 'removingItem')), + childBuilder: widget.childBuilder ?? + ParseLiveListWidget.defaultChildBuilder, + sizeFactor: animation, + duration: widget.duration, + loadedData: () => event.object, + ), + duration: widget.duration); + } + }); + }); + }); + } + + final QueryBuilder query; + ParseLiveList _liveList; + final GlobalKey _animatedListKey = + GlobalKey(); + final ChildBuilder removedItemBuilder; + + @override + Widget build(BuildContext context) { + return _liveList == null + ? widget.listLoadingElement ?? Container() + : buildAnimatedList(); + } + + Widget buildAnimatedList() { + return AnimatedList( + key: _animatedListKey, + physics: widget.scrollPhysics, + controller: widget.scrollController, + scrollDirection: widget.scrollDirection, + padding: widget.padding, + primary: widget.primary, + reverse: widget.reverse, + shrinkWrap: widget.shrinkWrap, + initialItemCount: _liveList?.size, + itemBuilder: + (BuildContext context, int index, Animation animation) { + return ParseLiveListElementWidget( + key: ValueKey( + _liveList?.getIdentifier(index) ?? '_NotFound'), + stream: () => _liveList?.getAt(index), + loadedData: () => _liveList?.getLoadedAt(index), + sizeFactor: animation, + duration: widget.duration, + childBuilder: + widget.childBuilder ?? ParseLiveListWidget.defaultChildBuilder, + ); + }); + } + + @override + void dispose() { + _liveList?.dispose(); + _liveList = null; + super.dispose(); + } +} + +class ParseLiveListElementWidget extends StatefulWidget { + const ParseLiveListElementWidget( + {Key key, + this.stream, + this.loadedData, + @required this.sizeFactor, + @required this.duration, + @required this.childBuilder}) + : super(key: key); + + final StreamGetter stream; + final DataGetter loadedData; + final Animation sizeFactor; + final Duration duration; + final ChildBuilder childBuilder; + + @override + _ParseLiveListElementWidgetState createState() { + return _ParseLiveListElementWidgetState(loadedData, stream); + } +} + +class _ParseLiveListElementWidgetState + extends State> + with SingleTickerProviderStateMixin { + _ParseLiveListElementWidgetState( + DataGetter loadedDataGetter, StreamGetter stream) { + _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); + if (stream != null) { + _streamSubscription = stream().listen( + (T data) { + if (widget != null) { + setState(() { + _snapshot = ParseLiveListElementSnapshot(loadedData: data); + }); + } else { + _snapshot = ParseLiveListElementSnapshot(loadedData: data); + } + }, + onError: (Object error) { + if (error is ParseError) { + if (widget != null) { + setState(() { + _snapshot = ParseLiveListElementSnapshot(error: error); + }); + } else { + _snapshot = ParseLiveListElementSnapshot(error: error); + } + } + }, + cancelOnError: false, + ); + } + } + + ParseLiveListElementSnapshot _snapshot; + + StreamSubscription _streamSubscription; + + @override + void dispose() { + _streamSubscription?.cancel(); + _streamSubscription = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Widget result = SizeTransition( + sizeFactor: widget.sizeFactor, + child: AnimatedSize( + duration: widget.duration, + vsync: this, + child: widget.childBuilder(context, _snapshot), + ), + ); + return result; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b23f9ab4a..50a5bfed7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,17 +13,17 @@ dependencies: # Networking web_socket_channel: ^1.1.0 - connectivity: ^0.4.9+2 + connectivity: ^0.4.9+2 # only used in the flutter part #Database sembast: ^2.4.7+6 xxtea: ^2.0.3 - shared_preferences: ^0.5.10 + shared_preferences: ^0.5.10 # only used in the flutter part # Utils - path_provider: ^1.6.14 + path_provider: ^1.6.14 # only used in the flutter part uuid: ^2.2.2 - package_info: ^0.4.3 + package_info: ^0.4.3 # only used in the flutter part meta: ^1.1.8 path: ^1.7.0 diff --git a/test/parse_client_configuration_test.dart b/test/parse_client_configuration_test.dart index 94c8b1e8c..87f9ab35c 100644 --- a/test/parse_client_configuration_test.dart +++ b/test/parse_client_configuration_test.dart @@ -10,8 +10,11 @@ void main() { clientKey: 'clientKey', liveQueryUrl: 'liveQueryUrl', appName: 'appName', + appPackageName: 'somePackageName', + appVersion: 'someAppVersion', masterKey: 'masterKey', sessionId: 'sessionId', + fileDirectory: 'someDirectory', debug: true); expect(ParseCoreData().applicationId, 'appId'); @@ -19,8 +22,11 @@ void main() { expect(ParseCoreData().clientKey, 'clientKey'); expect(ParseCoreData().liveQueryURL, 'liveQueryUrl'); expect(ParseCoreData().appName, 'appName'); + expect(ParseCoreData().appPackageName, 'somePackageName'); + expect(ParseCoreData().appVersion, 'someAppVersion'); expect(ParseCoreData().masterKey, 'masterKey'); expect(ParseCoreData().sessionId, 'sessionId'); expect(ParseCoreData().debug, true); + expect(ParseCoreData().fileDirectory, 'someDirectory'); }); } diff --git a/test/parse_query_test.dart b/test/parse_query_test.dart index 7506f095f..3a5133fbc 100644 --- a/test/parse_query_test.dart +++ b/test/parse_query_test.dart @@ -12,10 +12,22 @@ void main() { test('whereRelatedTo', () async { final MockClient client = MockClient(); - await Parse().initialize('appId', 'https://test.parse.com', debug: true); + await Parse().initialize( + 'appId', + 'https://test.parse.com', + debug: true, + // to prevent automatic detection + fileDirectory: 'someDirectory', + // to prevent automatic detection + appName: 'appName', + // to prevent automatic detection + appPackageName: 'somePackageName', + // to prevent automatic detection + appVersion: 'someAppVersion', + ); final QueryBuilder queryBuilder = - QueryBuilder(ParseObject('_User', client: client)); + QueryBuilder(ParseObject('_User', client: client)); queryBuilder.whereRelatedTo('likes', 'Post', '8TOXdXf3tz'); when(client.data).thenReturn(ParseCoreData()); @@ -27,7 +39,7 @@ void main() { final Uri expectedQuery = Uri( query: - 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); + 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); expect(result.query, expectedQuery.query); }); }); From d9a6171749eced86c2d8ff0c7ba917b11123cb72 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 4 Sep 2020 15:10:08 +0200 Subject: [PATCH 05/33] Add WEB-support for CoreStoreSembastImp (#436) * Add websupport for CoreStoreSembastImp * Add debug warning when using sembast on web ParseCoreData().debug should be null at that time * fixed issues --- README.md | 2 ++ lib/parse_server_sdk_dart.dart | 1 + lib/src/storage/core_store_sem_impl.dart | 13 ++++++++++--- pubspec.yaml | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e45d7f2d..192250ba4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ await Parse().initialize( ``` If you want to use secure storage or use the Flutter web/desktop SDK, please change to the below instance of CoreStorage as it has no dependencies on Flutter. + +**The `CoreStoreSembastImp` does not encrypt the data!** (Web is not safe anyway. Encrypt fields manually as needed.) ```dart await Parse().initialize( diff --git a/lib/parse_server_sdk_dart.dart b/lib/parse_server_sdk_dart.dart index 1da9ae8ae..c5da87525 100644 --- a/lib/parse_server_sdk_dart.dart +++ b/lib/parse_server_sdk_dart.dart @@ -14,6 +14,7 @@ import 'package:parse_server_sdk/src/network/parse_websocket.dart' import 'package:path/path.dart' as path; import 'package:sembast/sembast.dart'; import 'package:sembast/sembast_io.dart'; +import 'package:sembast_web/sembast_web.dart'; import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:xxtea/xxtea.dart'; diff --git a/lib/src/storage/core_store_sem_impl.dart b/lib/src/storage/core_store_sem_impl.dart index bf60602bd..466fc24ec 100644 --- a/lib/src/storage/core_store_sem_impl.dart +++ b/lib/src/storage/core_store_sem_impl.dart @@ -9,9 +9,16 @@ class CoreStoreSembastImp implements CoreStore { static Future getInstance(String dbPath, {DatabaseFactory factory, String password = 'flutter_sdk'}) async { if (_instance == null) { - factory ??= databaseFactoryIo; - final SembastCodec codec = getXXTeaSembastCodec(password: password); - final Database db = await factory.openDatabase(dbPath, codec: codec); + factory ??= !parseIsWeb ? databaseFactoryIo : databaseFactoryWeb; + assert(() { + if (parseIsWeb) { + print( + 'Warning: CoreStoreSembastImp of the Parse_Server_SDK does not encrypt the database on WEB.'); + } + return true; + }()); + final Database db = await factory.openDatabase(dbPath, + codec: !parseIsWeb ? getXXTeaSembastCodec(password: password) : null); _instance = CoreStoreSembastImp._internal(db, StoreRef.main()); } diff --git a/pubspec.yaml b/pubspec.yaml index 50a5bfed7..415a1cee1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: #Database sembast: ^2.4.7+6 + sembast_web: '>=1.0.0' xxtea: ^2.0.3 shared_preferences: ^0.5.10 # only used in the flutter part From f07909fe35082d00b1d25b0f421d781f35bc1dd1 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 4 Sep 2020 18:11:20 +0200 Subject: [PATCH 06/33] fix getInstance of CoreStoreSembastImp (#438) --- lib/parse_server_sdk.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/parse_server_sdk.dart b/lib/parse_server_sdk.dart index b84c7a83b..fa4fd2b21 100644 --- a/lib/parse_server_sdk.dart +++ b/lib/parse_server_sdk.dart @@ -135,8 +135,7 @@ class CoreStoreSembastImp implements sdk.CoreStoreSembastImp { static sdk.CoreStoreSembastImp _sembastImp; - static Future getInstance(String dbPath, - {DatabaseFactory factory, String password}) async { + static Future getInstance({DatabaseFactory factory, String password}) async { if (_sembastImp == null) { String dbDirectory = ''; if (!sdk.parseIsWeb && From bfc761561753cffdbbeb11aca17e7e89da82464d Mon Sep 17 00:00:00 2001 From: FNPCMDs Date: Sat, 5 Sep 2020 00:43:01 -0300 Subject: [PATCH 07/33] Reset _isConnected var If state is equal to ConnectivityResult.none, then there is no connection with LiveQuery server (for example when internet is disconnected). _setReconnect function will now retry connection. --- lib/src/network/parse_live_query.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index cd2b861be..aec1aca8c 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -94,6 +94,9 @@ class LiveQueryReconnectingController { _retryState = 0; } _isOnline = state != ParseConnectivityResult.none; + if(state == ConnectivityResult.none) { + _isConnected = false; + } if (debug) { print('$DEBUG_TAG: $state'); } From d0d12e52251fdd173fe533c702f6507639495f4a Mon Sep 17 00:00:00 2001 From: FNPCMDs Date: Sat, 5 Sep 2020 00:50:47 -0300 Subject: [PATCH 08/33] Fixed bad type ConnectivityResult to ParseConnectivityResult --- lib/src/network/parse_live_query.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/network/parse_live_query.dart b/lib/src/network/parse_live_query.dart index aec1aca8c..a1f151ecb 100644 --- a/lib/src/network/parse_live_query.dart +++ b/lib/src/network/parse_live_query.dart @@ -94,7 +94,7 @@ class LiveQueryReconnectingController { _retryState = 0; } _isOnline = state != ParseConnectivityResult.none; - if(state == ConnectivityResult.none) { + if(state == ParseConnectivityResult.none) { _isConnected = false; } if (debug) { From a6c33678c096afade40f885a33d3390f5dfae70e Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sat, 5 Sep 2020 08:38:41 +0200 Subject: [PATCH 09/33] Preparing the repository for seperated dart and flutter package (#439) * split code into package directories * removed unused dependencies * Updated READMEs --- README.md | 896 +----------------- packages/dart/README.md | 846 +++++++++++++++++ .../dart/lib/parse_server_sdk.dart | 0 .../dart/lib}/src/base/parse_constants.dart | 0 .../dart/lib}/src/data/app_info.dart | 0 .../dart/lib}/src/data/core_store.dart | 0 .../dart/lib}/src/data/parse_core_data.dart | 0 .../lib}/src/data/parse_subclass_handler.dart | 0 .../lib}/src/enums/parse_enum_api_rq.dart | 0 .../lib}/src/network/parse_connectivity.dart | 0 .../lib}/src/network/parse_http_client.dart | 0 .../lib}/src/network/parse_live_query.dart | 0 .../dart/lib}/src/network/parse_query.dart | 0 .../lib}/src/network/parse_websocket.dart | 0 .../src/network/parse_websocket_html.dart | 0 .../lib}/src/network/parse_websocket_io.dart | 0 .../dart/lib}/src/objects/parse_acl.dart | 0 .../dart/lib}/src/objects/parse_base.dart | 0 .../lib}/src/objects/parse_cloneable.dart | 0 .../dart/lib}/src/objects/parse_config.dart | 0 .../dart/lib}/src/objects/parse_error.dart | 0 .../dart/lib}/src/objects/parse_file.dart | 0 .../lib}/src/objects/parse_file_base.dart | 0 .../dart/lib}/src/objects/parse_file_web.dart | 0 .../dart/lib}/src/objects/parse_function.dart | 0 .../lib}/src/objects/parse_geo_point.dart | 0 .../lib}/src/objects/parse_installation.dart | 0 .../dart/lib}/src/objects/parse_merge.dart | 0 .../dart/lib}/src/objects/parse_object.dart | 0 .../dart/lib}/src/objects/parse_relation.dart | 0 .../dart/lib}/src/objects/parse_response.dart | 0 .../dart/lib}/src/objects/parse_session.dart | 0 .../dart/lib}/src/objects/parse_user.dart | 0 .../response/parse_error_response.dart | 0 .../response/parse_exception_response.dart | 0 .../response/parse_response_builder.dart | 0 .../response/parse_response_utils.dart | 0 .../response/parse_success_no_results.dart | 0 .../dart/lib}/src/storage/core_store.dart | 0 .../lib}/src/storage/core_store_sem_impl.dart | 0 .../dart/lib}/src/storage/xxtea_codec.dart | 0 .../lib}/src/utils/parse_date_format.dart | 0 .../dart/lib}/src/utils/parse_decoder.dart | 0 .../dart/lib}/src/utils/parse_encoder.dart | 0 .../lib}/src/utils/parse_file_extensions.dart | 0 .../dart/lib}/src/utils/parse_live_list.dart | 0 .../dart/lib}/src/utils/parse_logger.dart | 0 .../lib}/src/utils/parse_login_helpers.dart | 0 .../dart/lib}/src/utils/parse_utils.dart | 0 packages/dart/pubspec.yaml | 29 + packages/flutter/README.md | 893 +++++++++++++++++ .../flutter/example}/.gitignore | 0 .../flutter/example}/.metadata | 0 .../flutter/example}/README.md | 0 .../flutter/example}/android/.gitignore | 0 .../flutter/example}/android/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../flutter/example}/android/app/.classpath | 0 .../flutter/example}/android/app/.project | 0 .../org.eclipse.buildship.core.prefs | 0 .../flutter/example}/android/app/build.gradle | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../flutterpluginexample/BuildConfig.java | 0 .../flutterpluginexample/Manifest.java | 0 .../com/example/flutterpluginexample/R.java | 0 .../flutterpluginexample/MainActivity.java | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values/styles.xml | 0 .../flutter/example}/android/build.gradle | 0 .../example}/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../flutter/example}/android/settings.gradle | 0 .../flutter/example}/assets/parse.png | Bin .../flutter/example}/fonts/Roboto/LICENSE.txt | 0 .../example}/fonts/Roboto/Roboto-Black.ttf | Bin .../example}/fonts/Roboto/Roboto-Bold.ttf | Bin .../example}/fonts/Roboto/Roboto-Light.ttf | Bin .../example}/fonts/Roboto/Roboto-Medium.ttf | Bin .../example}/fonts/Roboto/Roboto-Regular.ttf | Bin .../example}/fonts/Roboto/Roboto-Thin.ttf | Bin .../flutter/example}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../example}/ios/Flutter/Debug.xcconfig | 0 .../example}/ios/Flutter/Release.xcconfig | 0 .../flutter/example}/ios/Podfile | 0 .../flutter/example}/ios/Podfile.lock | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../flutter/example}/ios/Runner/AppDelegate.h | 0 .../flutter/example}/ios/Runner/AppDelegate.m | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../flutter/example}/ios/Runner/Info.plist | 0 .../flutter/example}/ios/Runner/main.m | 0 .../example}/lib/data/base/api_error.dart | 0 .../example}/lib/data/base/api_response.dart | 0 .../flutter/example}/lib/data/model/day.dart | 0 .../example}/lib/data/model/diet_plan.dart | 0 .../flutter/example}/lib/data/model/user.dart | 0 .../contract_provider_diet_plan.dart | 0 .../diet_plan/provider_api_diet_plan.dart | 0 .../diet_plan/provider_db_diet_plan.dart | 0 .../diet_plan/repository_diet_plan.dart | 0 .../user/contract_provider_user.dart | 0 .../repositories/user/provider_api_user.dart | 0 .../repositories/user/provider_db_user.dart | 0 .../repositories/user/repository_user.dart | 0 .../constants/application_constants.dart | 0 .../lib/domain/utils/collection_utils.dart | 0 .../example}/lib/domain/utils/db_utils.dart | 0 .../flutter/example}/lib/main.dart | 2 +- .../example}/lib/pages/decision_page.dart | 0 .../flutter/example}/lib/pages/home_page.dart | 0 .../example}/lib/pages/login_page.dart | 0 .../flutter/example}/linux/.gitignore | 0 .../flutter/example}/linux/Makefile | 0 .../linux/flutter_embedder_example.cc | 0 .../flutter/example}/macos/.gitignore | 0 .../flutter/example}/macos/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../example}/macos/Base.lproj/MainMenu.xib | 0 .../example}/macos/ExampleWindow.swift | 0 .../example}/macos/Flutter/Debug.xcconfig | 0 .../example}/macos/Flutter/Release.xcconfig | 0 .../flutter/example}/macos/Info.plist | 0 .../flutter/example}/macos/PluginRegistrant.h | 0 .../flutter/example}/macos/PluginRegistrant.m | 0 packages/flutter/example/macos/Podfile | 82 ++ .../example}/macos/Runner-Bridging-Header.h | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../flutter/example}/macos/name_output.sh | 0 .../flutter/example}/pubspec.yaml | 2 +- .../repository_diet_plan_api_test.dart | 0 .../repository_diet_plan_db_test.dart | 0 .../diet_plan/repository_diet_plan_test.dart | 0 .../repository/repository_mock_utils.dart | 0 .../flutter/example}/windows/.gitignore | 0 .../flutter/example}/windows/Runner.sln | 0 .../flutter/example}/windows/Runner.vcxproj | 0 .../example}/windows/Runner.vcxproj.filters | 0 .../flutter/example}/windows/build.bat | 0 .../flutter/example}/windows/find_vcvars.dart | 0 .../windows/flutter_embedder_example.cpp | 0 .../example}/windows/generate_props.dart | 0 .../flutter/example}/windows/name_output.bat | 0 .../scripts/bundle_assets_and_deps.bat | 0 .../windows/scripts/prepare_dependencies.bat | 0 .../flutter/example_livelist}/.gitignore | 0 .../flutter/example_livelist}/.metadata | 0 .../flutter/example_livelist}/README.md | 0 .../example_livelist}/android/.gitignore | 0 .../android/app/build.gradle | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../example/example_livelist/MainActivity.kt | 0 .../MainActivity.kt | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../example_livelist}/android/build.gradle | 0 .../android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example_livelist}/android/settings.gradle | 0 .../example_livelist}/assets/parse.png | Bin .../fonts/Roboto/LICENSE.txt | 0 .../fonts/Roboto/Roboto-Black.ttf | Bin .../fonts/Roboto/Roboto-Bold.ttf | Bin .../fonts/Roboto/Roboto-Light.ttf | Bin .../fonts/Roboto/Roboto-Medium.ttf | Bin .../fonts/Roboto/Roboto-Regular.ttf | Bin .../fonts/Roboto/Roboto-Thin.ttf | Bin .../flutter/example_livelist}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../ios/Flutter/Debug.xcconfig | 0 .../ios/Flutter/Release.xcconfig | 0 .../flutter/example_livelist}/ios/Podfile | 0 .../example_livelist}/ios/Podfile.lock | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example_livelist}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../lib/application_constants.dart | 0 .../flutter/example_livelist}/lib/main.dart | 2 +- .../flutter/example_livelist}/pubspec.yaml | 2 +- .../flutter/example_livelist}/web/favicon.png | Bin .../example_livelist}/web/icons/Icon-192.png | Bin .../example_livelist}/web/icons/Icon-512.png | Bin .../flutter/example_livelist}/web/index.html | 0 .../example_livelist}/web/manifest.json | 0 .../flutter/lib}/generated/i18n.dart | 0 .../flutter/lib}/parse_server_sdk.dart | 20 +- .../lib}/src/storage/core_store_sp_impl.dart | 7 +- .../lib/src/utils/parse_live_list.dart | 64 +- pubspec.yaml => packages/flutter/pubspec.yaml | 12 +- .../parse_client_configuration_test.dart | 2 +- .../flutter/test}/parse_query_test.dart | 2 +- 263 files changed, 1920 insertions(+), 941 deletions(-) create mode 100644 packages/dart/README.md rename lib/parse_server_sdk_dart.dart => packages/dart/lib/parse_server_sdk.dart (100%) rename {lib => packages/dart/lib}/src/base/parse_constants.dart (100%) rename {lib => packages/dart/lib}/src/data/app_info.dart (100%) rename {lib => packages/dart/lib}/src/data/core_store.dart (100%) rename {lib => packages/dart/lib}/src/data/parse_core_data.dart (100%) rename {lib => packages/dart/lib}/src/data/parse_subclass_handler.dart (100%) rename {lib => packages/dart/lib}/src/enums/parse_enum_api_rq.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_connectivity.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_http_client.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_live_query.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_query.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_websocket.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_websocket_html.dart (100%) rename {lib => packages/dart/lib}/src/network/parse_websocket_io.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_acl.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_base.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_cloneable.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_config.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_error.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_file.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_file_base.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_file_web.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_function.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_geo_point.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_installation.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_merge.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_object.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_relation.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_response.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_session.dart (100%) rename {lib => packages/dart/lib}/src/objects/parse_user.dart (100%) rename {lib => packages/dart/lib}/src/objects/response/parse_error_response.dart (100%) rename {lib => packages/dart/lib}/src/objects/response/parse_exception_response.dart (100%) rename {lib => packages/dart/lib}/src/objects/response/parse_response_builder.dart (100%) rename {lib => packages/dart/lib}/src/objects/response/parse_response_utils.dart (100%) rename {lib => packages/dart/lib}/src/objects/response/parse_success_no_results.dart (100%) rename {lib => packages/dart/lib}/src/storage/core_store.dart (100%) rename {lib => packages/dart/lib}/src/storage/core_store_sem_impl.dart (100%) rename {lib => packages/dart/lib}/src/storage/xxtea_codec.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_date_format.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_decoder.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_encoder.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_file_extensions.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_live_list.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_logger.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_login_helpers.dart (100%) rename {lib => packages/dart/lib}/src/utils/parse_utils.dart (100%) create mode 100644 packages/dart/pubspec.yaml create mode 100644 packages/flutter/README.md rename {example => packages/flutter/example}/.gitignore (100%) rename {example => packages/flutter/example}/.metadata (100%) rename {example => packages/flutter/example}/README.md (100%) rename {example => packages/flutter/example}/android/.gitignore (100%) rename {example => packages/flutter/example}/android/.project (100%) rename {example => packages/flutter/example}/android/.settings/org.eclipse.buildship.core.prefs (100%) rename {example => packages/flutter/example}/android/app/.classpath (100%) rename {example => packages/flutter/example}/android/app/.project (100%) rename {example => packages/flutter/example}/android/app/.settings/org.eclipse.buildship.core.prefs (100%) rename {example => packages/flutter/example}/android/app/build.gradle (100%) rename {example => packages/flutter/example}/android/app/src/main/AndroidManifest.xml (100%) rename {example => packages/flutter/example}/android/app/src/main/gen/com/example/flutterpluginexample/BuildConfig.java (100%) rename {example => packages/flutter/example}/android/app/src/main/gen/com/example/flutterpluginexample/Manifest.java (100%) rename {example => packages/flutter/example}/android/app/src/main/gen/com/example/flutterpluginexample/R.java (100%) rename {example => packages/flutter/example}/android/app/src/main/java/com/example/flutterpluginexample/MainActivity.java (100%) rename {example => packages/flutter/example}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {example => packages/flutter/example}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example => packages/flutter/example}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example => packages/flutter/example}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example => packages/flutter/example}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example => packages/flutter/example}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {example => packages/flutter/example}/android/app/src/main/res/values/styles.xml (100%) rename {example => packages/flutter/example}/android/build.gradle (100%) rename {example => packages/flutter/example}/android/gradle.properties (100%) rename {example => packages/flutter/example}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {example => packages/flutter/example}/android/settings.gradle (100%) rename {example => packages/flutter/example}/assets/parse.png (100%) rename {example => packages/flutter/example}/fonts/Roboto/LICENSE.txt (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Black.ttf (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Bold.ttf (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Light.ttf (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Medium.ttf (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Regular.ttf (100%) rename {example => packages/flutter/example}/fonts/Roboto/Roboto-Thin.ttf (100%) rename {example => packages/flutter/example}/ios/.gitignore (100%) rename {example => packages/flutter/example}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {example => packages/flutter/example}/ios/Flutter/Debug.xcconfig (100%) rename {example => packages/flutter/example}/ios/Flutter/Release.xcconfig (100%) rename {example => packages/flutter/example}/ios/Podfile (100%) rename {example => packages/flutter/example}/ios/Podfile.lock (100%) rename {example => packages/flutter/example}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {example => packages/flutter/example}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flutter/example}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => packages/flutter/example}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flutter/example}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => packages/flutter/example}/ios/Runner/AppDelegate.h (100%) rename {example => packages/flutter/example}/ios/Runner/AppDelegate.m (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {example => packages/flutter/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {example => packages/flutter/example}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {example => packages/flutter/example}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {example => packages/flutter/example}/ios/Runner/Info.plist (100%) rename {example => packages/flutter/example}/ios/Runner/main.m (100%) rename {example => packages/flutter/example}/lib/data/base/api_error.dart (100%) rename {example => packages/flutter/example}/lib/data/base/api_response.dart (100%) rename {example => packages/flutter/example}/lib/data/model/day.dart (100%) rename {example => packages/flutter/example}/lib/data/model/diet_plan.dart (100%) rename {example => packages/flutter/example}/lib/data/model/user.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/diet_plan/provider_api_diet_plan.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/diet_plan/provider_db_diet_plan.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/diet_plan/repository_diet_plan.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/user/contract_provider_user.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/user/provider_api_user.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/user/provider_db_user.dart (100%) rename {example => packages/flutter/example}/lib/data/repositories/user/repository_user.dart (100%) rename {example => packages/flutter/example}/lib/domain/constants/application_constants.dart (100%) rename {example => packages/flutter/example}/lib/domain/utils/collection_utils.dart (100%) rename {example => packages/flutter/example}/lib/domain/utils/db_utils.dart (100%) rename {example => packages/flutter/example}/lib/main.dart (99%) rename {example => packages/flutter/example}/lib/pages/decision_page.dart (100%) rename {example => packages/flutter/example}/lib/pages/home_page.dart (100%) rename {example => packages/flutter/example}/lib/pages/login_page.dart (100%) rename {example => packages/flutter/example}/linux/.gitignore (100%) rename {example => packages/flutter/example}/linux/Makefile (100%) rename {example => packages/flutter/example}/linux/flutter_embedder_example.cc (100%) rename {example => packages/flutter/example}/macos/.gitignore (100%) rename {example => packages/flutter/example}/macos/AppDelegate.swift (100%) rename {example => packages/flutter/example}/macos/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/flutter/example}/macos/Base.lproj/MainMenu.xib (100%) rename {example => packages/flutter/example}/macos/ExampleWindow.swift (100%) rename {example => packages/flutter/example}/macos/Flutter/Debug.xcconfig (100%) rename {example => packages/flutter/example}/macos/Flutter/Release.xcconfig (100%) rename {example => packages/flutter/example}/macos/Info.plist (100%) rename {example => packages/flutter/example}/macos/PluginRegistrant.h (100%) rename {example => packages/flutter/example}/macos/PluginRegistrant.m (100%) create mode 100644 packages/flutter/example/macos/Podfile rename {example => packages/flutter/example}/macos/Runner-Bridging-Header.h (100%) rename {example => packages/flutter/example}/macos/Runner.xcodeproj/project.pbxproj (100%) rename {example => packages/flutter/example}/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {example => packages/flutter/example}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/flutter/example}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => packages/flutter/example}/macos/name_output.sh (100%) rename {example => packages/flutter/example}/pubspec.yaml (98%) rename {example => packages/flutter/example}/test/data/repository/diet_plan/repository_diet_plan_api_test.dart (100%) rename {example => packages/flutter/example}/test/data/repository/diet_plan/repository_diet_plan_db_test.dart (100%) rename {example => packages/flutter/example}/test/data/repository/diet_plan/repository_diet_plan_test.dart (100%) rename {example => packages/flutter/example}/test/data/repository/repository_mock_utils.dart (100%) rename {example => packages/flutter/example}/windows/.gitignore (100%) rename {example => packages/flutter/example}/windows/Runner.sln (100%) rename {example => packages/flutter/example}/windows/Runner.vcxproj (100%) rename {example => packages/flutter/example}/windows/Runner.vcxproj.filters (100%) rename {example => packages/flutter/example}/windows/build.bat (100%) rename {example => packages/flutter/example}/windows/find_vcvars.dart (100%) rename {example => packages/flutter/example}/windows/flutter_embedder_example.cpp (100%) rename {example => packages/flutter/example}/windows/generate_props.dart (100%) rename {example => packages/flutter/example}/windows/name_output.bat (100%) rename {example => packages/flutter/example}/windows/scripts/bundle_assets_and_deps.bat (100%) rename {example => packages/flutter/example}/windows/scripts/prepare_dependencies.bat (100%) rename {example_livelist => packages/flutter/example_livelist}/.gitignore (100%) rename {example_livelist => packages/flutter/example_livelist}/.metadata (100%) rename {example_livelist => packages/flutter/example_livelist}/README.md (100%) rename {example_livelist => packages/flutter/example_livelist}/android/.gitignore (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/build.gradle (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/debug/AndroidManifest.xml (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/AndroidManifest.xml (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/kotlin/com/example/flutterpluginexample_livelist/MainActivity.kt (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/main/res/values/styles.xml (100%) rename {example_livelist => packages/flutter/example_livelist}/android/app/src/profile/AndroidManifest.xml (100%) rename {example_livelist => packages/flutter/example_livelist}/android/build.gradle (100%) rename {example_livelist => packages/flutter/example_livelist}/android/gradle.properties (100%) rename {example_livelist => packages/flutter/example_livelist}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {example_livelist => packages/flutter/example_livelist}/android/settings.gradle (100%) rename {example_livelist => packages/flutter/example_livelist}/assets/parse.png (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/LICENSE.txt (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Black.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Bold.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Light.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Medium.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Regular.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/fonts/Roboto/Roboto-Thin.ttf (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/.gitignore (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Flutter/Debug.xcconfig (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Flutter/Release.xcconfig (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Podfile (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Podfile.lock (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/AppDelegate.swift (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Info.plist (100%) rename {example_livelist => packages/flutter/example_livelist}/ios/Runner/Runner-Bridging-Header.h (100%) rename {example_livelist => packages/flutter/example_livelist}/lib/application_constants.dart (100%) rename {example_livelist => packages/flutter/example_livelist}/lib/main.dart (98%) rename {example_livelist => packages/flutter/example_livelist}/pubspec.yaml (98%) rename {example_livelist => packages/flutter/example_livelist}/web/favicon.png (100%) rename {example_livelist => packages/flutter/example_livelist}/web/icons/Icon-192.png (100%) rename {example_livelist => packages/flutter/example_livelist}/web/icons/Icon-512.png (100%) rename {example_livelist => packages/flutter/example_livelist}/web/index.html (100%) rename {example_livelist => packages/flutter/example_livelist}/web/manifest.json (100%) rename {lib => packages/flutter/lib}/generated/i18n.dart (100%) rename {lib => packages/flutter/lib}/parse_server_sdk.dart (91%) rename {lib => packages/flutter/lib}/src/storage/core_store_sp_impl.dart (90%) rename lib/src/utils/parse_live_list_flutter.dart => packages/flutter/lib/src/utils/parse_live_list.dart (76%) rename pubspec.yaml => packages/flutter/pubspec.yaml (79%) rename {test => packages/flutter/test}/parse_client_configuration_test.dart (94%) rename {test => packages/flutter/test}/parse_query_test.dart (95%) diff --git a/README.md b/README.md index 192250ba4..cd73effc2 100644 --- a/README.md +++ b/README.md @@ -1,893 +1,21 @@ -![Parse Logo](https://upload.wikimedia.org/wikipedia/commons/1/17/Google-flutter-logo.png)![Flutter Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +![Parse Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +--- -## Parse For Flutter! -Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). +This repository contains packages that allow communication with a Parse Server, +(https://parseplatform.org) either hosted on your own server or another, +like (http://Back4App.com). This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) -## Join in! -Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) +## Packages -## Getting Started -To install, either add to your pubspec.yaml -```yml -dependencies: - parse_server_sdk: ^1.0.28 -``` -or clone this repository and add to your project. As this is an early development with multiple contributors, it is probably best to download/clone and keep updating as an when a new feature is added. +These are the available packages in this repository. +| Plugin | Pub | explanation| +|--------|-----|------------| +| [parse_server_sdk](./packages/dart) | [![pub package](https://img.shields.io/pub/v/parse_server_sdk.svg)](https://pub.dev/packages/parse_server_sdk) | a dart package that lets you communicate with the parse server | +| [parse_server_sdk_flutter](./packages/dart) | [![pub package](https://img.shields.io/pub/v/parse_server_sdk_flutter.svg)](https://pub.dev/packages/parse_server_sdk_flutter) | a flutter package that lets you communicate with the parse server | -Once you have the library added to your project, upon first call to your app (Similar to what your application class would be) add the following... - -```dart -await Parse().initialize( - keyApplicationId, - keyParseServerUrl); -``` - -If you want to use secure storage or use the Flutter web/desktop SDK, please change to the below instance of CoreStorage as it has no dependencies on Flutter. - -**The `CoreStoreSembastImp` does not encrypt the data!** (Web is not safe anyway. Encrypt fields manually as needed.) -```dart - -await Parse().initialize( - keyParseApplicationId, - keyParseServerUrl, - coreStore: await CoreStoreSembastImp.getInstance()); -``` -It's possible to add other parameters to work with your instance of Parse Server:- - -```dart - await Parse().initialize( - keyApplicationId, - keyParseServerUrl, - masterKey: keyParseMasterKey, // Required for Back4App and others - clientKey: keyParseClientKey, // Required for some setups - debug: true, // When enabled, prints logs to console - liveQueryUrl: keyLiveQueryUrl, // Required if using LiveQuery - autoSendSessionId: true, // Required for authentication and ACL - securityContext: securityContext, // Again, required for some setups - coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB -``` - - -#### Early Web support -Currently this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. -When running directly via docker, set the env var `PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id`. -When running via express, set [ParseServerOptions](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `allowHeaders: ['X-Parse-Installation-Id']`. - -Be aware that for web ParseInstallation does include app name, version or package identifier. - - -## Objects -You can create custom objects by calling: -```dart -var dietPlan = ParseObject('DietPlan') - ..set('Name', 'Ketogenic') - ..set('Fat', 65); -await dietPlan.save(); -``` -Or update existing object by its objectId by calling: -```dart -var dietPlan = ParseObject('DietPlan') - ..objectId = 'R5EonpUDWy' - ..set('Fat', 70); -await dietPlan.save(); -``` -Verify that the object has been successfully saved using -```dart -var response = await dietPlan.save(); -if (response.success) { - dietPlan = response.results.first; -} -``` -Types supported: - * String - * Double - * Int - * Boolean - * DateTime - * File - * Geopoint - * ParseObject/ParseUser (Pointer) - * Map - * List (all types supported) - -You then have the ability to do the following with that object: -The features available are:- - * Get - * GetAll - * Create - * Save - * Query - By object Id - * Delete - * Complex queries as shown above - * Pin - * Plenty more - * Counters - * Array Operators - -## Custom Objects -You can create your own `ParseObjects` or convert your existing objects into Parse Objects by doing the following: - -```dart -class DietPlan extends ParseObject implements ParseCloneable { - - DietPlan() : super(_keyTableName); - DietPlan.clone(): this(); - - /// Looks strangely hacky but due to Flutter not using reflection, we have to - /// mimic a clone - @override clone(Map map) => DietPlan.clone()..fromJson(map); - - static const String _keyTableName = 'Diet_Plans'; - static const String keyName = 'Name'; - - String get name => get(keyName); - set name(String name) => set(keyName, name); -} - -``` - -When receiving an `ParseObject` from the SDK, you can often provide an instance of your custom object as an copy object. -To always use your custom object class, you can register your subclass at the initialization of the SDK. -```dart -Parse().initialize( - ..., - registeredSubClassMap: { - 'Diet_Plans': () => DietPlan(), - }, - parseUserConstructor: (username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress), -); -``` -Additionally you can register `SubClasses` after the initialization of the SDK. -```dart -ParseCoreData().registerSubClass('Diet_Plans', () => DietPlan()); -ParseCoreData().registerUserSubClass((username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress)); -``` -Providing a `ParseObject` as described above should still work, even if you have registered a different `SubClass`. - -For custom file classes have a lock at [here](#File). - -## Add new values to objects -To add a variable to an object call and retrieve it, call - -```dart -dietPlan.set('RandomInt', 8); -var randomInt = dietPlan.get('RandomInt'); -``` - -## Save objects using pins -You can now save an object by calling `.pin()` on an instance of an object - -```dart -dietPlan.pin(); -``` - -and to retrieve it - -```dart -var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT'); -``` - -## Storage -We now have 2 types of storage, secure and unsecure. We currently rely on 2 third party options: - -- SharedPreferences -- Sembast -Sembast offers secured storage, whilst SharePreferences wraps NSUserDefaults (on iOS) and SharedPreferences (on Android). - -The storage method is defined in the parameter __coreStore__ in Parse().initialize - -Check sample code for options - -## Increment Counter values in objects -Retrieve it, call - -```dart -var response = await dietPlan.increment("count", 1); - -``` -or using with save function - -```dart -dietPlan.setIncrement('count', 1); -dietPlan.setDecrement('count', 1); -var response = dietPlan.save() - -``` - -## Array Operator in objects -Retrieve it, call - -```dart -var response = await dietPlan.add("listKeywords", ["a", "a","d"]); - -var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); - -var response = await dietPlan.remove("listKeywords", ["a"]); - -``` -or using with save function - -```dart -dietPlan.setAdd('listKeywords', ['a','a','d']); -dietPlan.setAddUnique('listKeywords', ['a','a','d']); -dietPlan.setRemove('listKeywords', ['a']); -var response = dietPlan.save() -``` - -## Queries -Once you have setup the project and initialised the instance, you can then retreive data from your server by calling: -```dart -var apiResponse = await ParseObject('ParseTableName').getAll(); - -if (apiResponse.success){ - for (var testObject in apiResponse.result) { - print(ApplicationConstants.APP_NAME + ": " + testObject.toString()); - } -} -``` -Or you can get an object by its objectId: - -```dart -var dietPlan = await DietPlan().getObject('R5EonpUDWy'); - -if (dietPlan.success) { - print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString()); -} else { - print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message); -} -``` - -## Complex queries -You can create complex queries to really put your database to the test: - -```dart -var queryBuilder = QueryBuilder(DietPlan()) - ..startsWith(DietPlan.keyName, "Keto") - ..greaterThan(DietPlan.keyFat, 64) - ..lessThan(DietPlan.keyFat, 66) - ..equals(DietPlan.keyCarbs, 5); - -var response = await queryBuilder.query(); - -if (response.success) { - print(ApplicationConstants.keyAppName + ": " + ((response.results as List).first as DietPlan).toString()); -} else { - print(ApplicationConstants.keyAppName + ": " + response.exception.message); -} -``` - -if you want to find objects that match one of several queries, you can use __QueryBuilder.or__ method to construct a query that is an OR of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do: -```dart -ParseObject playerObject = ParseObject("Player"); - -QueryBuilder lotsOfWins = - QueryBuilder(playerObject)) - ..whereGreaterThan('wins', 50); - -QueryBuilder fewWins = - QueryBuilder(playerObject) - ..whereLessThan('wins', 5); - -QueryBuilder mainQuery = QueryBuilder.or( - playerObject, - [lotsOfWins, fewWins], - ); - -var apiResponse = await mainQuery.query(); -``` - -The features available are:- - * Equals - * Contains - * LessThan - * LessThanOrEqualTo - * GreaterThan - * GreaterThanOrEqualTo - * NotEqualTo - * StartsWith - * EndsWith - * Exists - * Near - * WithinMiles - * WithinKilometers - * WithinRadians - * WithinGeoBox - * MatchesQuery - * DoesNotMatchQuery - * MatchesKeyInQuery - * DoesNotMatchKeyInQuery - * Regex - * Order - * Limit - * Skip - * Ascending - * Descending - * Plenty more! - -## Relational queries -If you want to retrieve objects where a field contains an object that matches another query, you can use the -__whereMatchesQuery__ condition. -For example, imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. -You can find comments on posts with images by doing: - -```dart -QueryBuilder queryPost = - QueryBuilder(ParseObject('Post')) - ..whereValueExists('image', true); - -QueryBuilder queryComment = - QueryBuilder(ParseObject('Comment')) - ..whereMatchesQuery('post', queryPost); - -var apiResponse = await queryComment.query(); -``` - -If you want to retrieve objects where a field contains an object that does not match another query, you can use the -__whereDoesNotMatchQuery__ condition. -Imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. -You can find comments on posts without images by doing: - -```dart -QueryBuilder queryPost = - QueryBuilder(ParseObject('Post')) - ..whereValueExists('image', true); - -QueryBuilder queryComment = - QueryBuilder(ParseObject('Comment')) - ..whereDoesNotMatchQuery('post', queryPost); - -var apiResponse = await queryComment.query(); -``` - -You can use the __whereMatchesKeyInQuery__ method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:: - -```dart -QueryBuilder teamQuery = - QueryBuilder(ParseObject('Team')) - ..whereGreaterThan('winPct', 0.5); - -QueryBuilder userQuery = - QueryBuilderParseUser.forQuery()) - ..whereMatchesKeyInQuery('hometown', 'city', teamQuery); - -var apiResponse = await userQuery.query(); -``` - -Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use __whereDoesNotMatchKeyInQuery__. For example, to find users whose hometown teams have losing records: - -```dart -QueryBuilder teamQuery = - QueryBuilder(ParseObject('Team')) - ..whereGreaterThan('winPct', 0.5); - -QueryBuilder losingUserQuery = - QueryBuilderParseUser.forQuery()) - ..whereDoesNotMatchKeyInQuery('hometown', 'city', teamQuery); - -var apiResponse = await losingUserQuery.query(); -``` - -To filter rows based on objectId’s from pointers in a second table, you can use dot notation: -```dart -QueryBuilder rolesOfTypeX = - QueryBuilder(ParseObject('Role')) - ..whereEqualTo('type', 'x'); - -QueryBuilder groupsWithRoleX = - QueryBuilder(ParseObject('Group'))) - ..whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); - -var apiResponse = await groupsWithRoleX.query(); -``` - -## Counting Objects -If you only care about the number of games played by a particular player: - -```dart -QueryBuilder queryPlayers = - QueryBuilder(ParseObject('GameScore')) - ..whereEqualTo('playerName', 'Jonathan Walsh'); -var apiResponse = await queryPlayers.count(); -if (apiResponse.success && apiResponse.result != null) { - int countGames = apiResponse.count; -} -``` - -## Live Queries -This tool allows you to subscribe to a QueryBuilder you are interested in. Once subscribed, the server will notify clients -whenever a ParseObject that matches the QueryBuilder is created or updated, in real-time. - -Parse LiveQuery contains two parts, the LiveQuery server and the LiveQuery clients. In order to use live queries, you need -to set up both of them. - -The Parse Server configuration guide on the server is found here https://docs.parseplatform.org/parse-server/guide/#live-queries and is not part of this documentation. - -Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize: -```dart -Parse().initialize( - keyApplicationId, - keyParseServerUrl, - clientKey: keyParseClientKey, - debug: true, - liveQueryUrl: keyLiveQueryUrl, - autoSendSessionId: true); -``` - -Declare LiveQuery: -```dart -final LiveQuery liveQuery = LiveQuery(); -``` - -Set the QueryBuilder that will be monitored by LiveQuery: -```dart -QueryBuilder query = - QueryBuilder(ParseObject('TestAPI')) - ..whereEqualTo('intNumber', 1); -``` -__Create a subscription__ -You’ll get the LiveQuery events through this subscription. -The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you. - -```dart -Subscription subscription = await liveQuery.client.subscribe(query); -``` - -__Event Handling__ -We define several types of events you’ll get through a subscription object: - -__Create event__ -When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. -The object is the ParseObject which was created. -```dart -subscription.on(LiveQueryEvent.create, (value) { - print('*** CREATE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); -}); -``` - -__Update event__ -When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the -QueryBuilder before and after changes), you’ll get this event. -The object is the ParseObject which was updated. Its content is the latest value of the ParseObject. -```dart -subscription.on(LiveQueryEvent.update, (value) { - print('*** UPDATE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); -}); -``` - -__Enter event__ -When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, -you’ll get this event. The object is the ParseObject which enters the QueryBuilder. -Its content is the latest value of the ParseObject. -```dart -subscription.on(LiveQueryEvent.enter, (value) { - print('*** ENTER ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); -}); -``` - -__Leave event__ -When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, -you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. -Its content is the latest value of the ParseObject. -```dart -subscription.on(LiveQueryEvent.leave, (value) { - print('*** LEAVE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); -}); -``` - -__Delete event__ -When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. -The object is the ParseObject which is deleted -```dart -subscription.on(LiveQueryEvent.delete, (value) { - print('*** DELETE ***: ${DateTime.now().toString()}\n $value '); - print((value as ParseObject).objectId); - print((value as ParseObject).updatedAt); - print((value as ParseObject).createdAt); - print((value as ParseObject).get('objectId')); - print((value as ParseObject).get('updatedAt')); - print((value as ParseObject).get('createdAt')); -}); -``` - -__Unsubscribe__ -If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. -After that, you won’t get any events from the subscription object and will close the WebSocket connection to the -LiveQuery server. - -```dart -liveQuery.client.unSubscribe(subscription); -``` - -__Disconnection__ -In case the client's connection to the server breaks, -LiveQuery will automatically try to reconnect. -LiveQuery will wait at increasing intervals between reconnection attempts. -By default, these intervals are set to `[0, 500, 1000, 2000, 5000, 10000]` for mobile and `[0, 500, 1000, 2000, 5000]` for web. -You can change these by providing a custom list using the `liveListRetryIntervals` parameter at `Parse.initialize()` ("-1" means "do not try to reconnect"). - -## ParseLiveList -ParseLiveList makes implementing a dynamic List as simple as possible. - -#### General Use -It ships with the ParseLiveList class itself, this class manages all elements of the list, sorts them, -keeps itself up to date and Notifies you on changes. - -ParseLiveListWidget is a widget that handles all the communication with the ParseLiveList for you. -Using ParseLiveListWidget you can create a dynamic List by just providing a QueryBuilder. - -```dart -ParseLiveListWidget( - query: query, - ); -``` -To customize the List Elements, you can provide a childBuilder. -```dart -ParseLiveListWidget( - query: query, - reverse: false, - childBuilder: - (BuildContext context, ParseLiveListElementSnapshot snapshot) { - if (snapshot.failed) { - return const Text('something went wrong!'); - } else if (snapshot.hasData) { - return ListTile( - title: Text( - snapshot.loadedData.get("text"), - ), - ); - } else { - return const ListTile( - leading: CircularProgressIndicator(), - ); - } - }, -); -``` -Similar to the standard ListView, you can provide arguments like reverse or shrinkWrap. -By providing the listLoadingElement, you can show the user something while the list is loading. -```dart -ParseLiveListWidget( - query: query, - childBuilder: childBuilder, - listLoadingElement: Center( - child: CircularProgressIndicator(), - ), -); -``` -By providing the duration argument, you can change the animation speed. -```dart -ParseLiveListWidget( - query: query, - childBuilder: childBuilder, - duration: Duration(seconds: 1), -); -``` -### included Sub-Objects -By default, ParseLiveQuery will provide you with all the objects you included in your Query like this: -```dart -queryBuilder.includeObject(/*List of all the included sub-objects*/); -``` -ParseLiveList will not listen for updates on this objects by default. -To activate listening for updates on all included objects, add `listenOnAllSubItems: true` to your ParseLiveListWidgets constructor. -If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. -Just as QueryBuilder, ParseLiveList supports nested sub-objects too. - -**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. - -## Users -You can create and control users just as normal using this SDK. - -To register a user, first create one : -```dart -var user = ParseUser().create("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com"); -``` -Then have the user sign up: - -```dart -var response = await user.signUp(); -if (response.success) user = response.result; -``` -You can also login with the user: -```dart -var response = await user.login(); -if (response.success) user = response.result; -``` -You can also logout with the user: -```dart -var response = await user.logout(); -if (response.success) { - print('User logout'); -} -``` -Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init() on startup to check for a logged in user. -```dart -user = ParseUser.currentUser(); -``` - -To add additional columns to the user: -```dart -var user = ParseUser("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com") - ..set("userLocation", "FlutterLand"); -``` - -Other user features are:- - * Request Password Reset - * Verification Email Request - * Get all users - * Save - * Destroy user - * Queries - - ## Facebook, OAuth and 3rd Party Login/User - - Usually, each provider will provide their own library for logins, but the loginWith method on ParseUser accepts a name of provider, then a Map with the authentication details required. - For Facebook and the example below, we used the library provided at https://pub.dev/packages/flutter_facebook_login - - ``` - Future goToFacebookLogin() async { - final FacebookLogin facebookLogin = FacebookLogin(); - final FacebookLoginResult result = await facebookLogin.logInWithReadPermissions(['email']); - - switch (result.status) { - case FacebookLoginStatus.loggedIn: - final ParseResponse response = await ParseUser.loginWith( - 'facebook', - facebook(result.accessToken.token, - result.accessToken.userId, - result.accessToken.expires)); - - if (response.success) { - // User is logged in, test with ParseUser.currentUser() - } - break; - case FacebookLoginStatus.cancelledByUser: - // User cancelled - break; - case FacebookLoginStatus.error: - // Error - break; - } - } -``` - -For Google and the example below, we used the library provided at https://pub.dev/packages/google_sign_in - -``` -class OAuthLogin { - final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'https://www.googleapis.com/auth/contacts.readonly'] ); - - sigInGoogle() async { - GoogleSignInAccount account = await _googleSignIn.signIn(); - GoogleSignInAuthentication authentication = await account.authentication; - await ParseUser.loginWith( - 'google', - google(_googleSignIn.currentUser.id, - authentication.accessToken, - authentication.idToken)); - } -} -``` - -## Security for Objects - ParseACL -For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. -To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. - -If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write. -The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. -To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that -limits access to that user. An object’s ACL is updated when the object is saved, like any other property. - -```dart -ParseUser user = await ParseUser.currentUser() as ParseUser; -ParseACL parseACL = ParseACL(owner: user); - -ParseObject parseObject = ParseObject("TestAPI"); -... -parseObject.setACL(parseACL); -var apiResponse = await parseObject.save(); -``` -Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using -__setReadAccess__ and __setWriteAccess__ -```dart -ParseUser user = await ParseUser.currentUser() as ParseUser; -ParseACL parseACL = ParseACL(); -//grant total access to current user -parseACL.setReadAccess(userId: user.objectId, allowed: true); -parseACL.setWriteAccess(userId: user.objectId, allowed: true); -//grant read access to userId: 'TjRuDjuSAO' -parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true); -parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false); - -ParseObject parseObject = ParseObject("TestAPI"); -... -parseObject.setACL(parseACL); -var apiResponse = await parseObject.save(); -``` -You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess. -```dart -ParseACL parseACL = ParseACL(); -parseACL.setPublicReadAccess(allowed: true); -parseACL.setPublicWriteAccess(allowed: true); - -ParseObject parseObject = ParseObject("TestAPI"); -... -parseObject.setACL(parseACL); -var apiResponse = await parseObject.save(); -``` -Operations that are forbidden, such as deleting an object that you do not have write access to, result in a -ParseError with code 101: 'ObjectNotFound'. -For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which -object ids do not exist at all. - -You can retrieve the ACL list of an object using: -```dart -ParseACL parseACL = parseObject.getACL(); -``` - -## Config -The SDK supports Parse Config. A map of all configs can be grabbed from the server by calling : -```dart -var response = await ParseConfig().getConfigs(); -``` - -and to add a config: -```dart -ParseConfig().addConfig('TestConfig', 'testing'); -``` - -## Cloud Functions -The SDK supports call Cloud Functions. - -Executes a cloud function that returns a ParseObject type -```dart -final ParseCloudFunction function = ParseCloudFunction('hello'); -final ParseResponse result = - await function.executeObjectFunction(); -if (result.success) { - if (result.result is ParseObject) { - final ParseObject parseObject = result.result; - print(parseObject.className); - } -} -``` - -Executes a cloud function with parameters -```dart -final ParseCloudFunction function = ParseCloudFunction('hello'); -final Map params = {'plan': 'paid'}; -function.execute(parameters: params); -``` - -## Relation - -The SDK supports Relation. - -To add relation to object: - -```dart -dietPlan.addRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); -``` - -To remove relation to object: - -```dart -dietPlan.removeRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); -``` - -To Retrive a relation instance for user, call: -```dart -final relation = dietPlan.getRelation('fruits'); -``` - -and then you can add a relation to the passed in object: -``` -relation.add(dietPlan); -final result = await user.save(); -``` - -To retrieve objects that are members of Relation field of a parent object: -```dart -QueryBuilder query = - QueryBuilder(ParseObject('Fruits')) - ..whereRelatedTo('fruits', 'DietPlan', DietPlan.objectId); -``` - -## File -There are three different file classes in this SDK: -- `ParseFileBase` is and abstract class and is the foundation of every file class that can be handled by this SDK. -- `ParseFile` (former the only file class in the SDK) extends ParseFileBase and is by default used as the file class on every platform but web. -This class uses a `File` from `dart:io` for storing the raw file. -- `ParseWebFile` is the equivalent to ParseFile used at Flutter Web. -This class uses an `Uint8List` for storing the raw file. - -These classes are used by default to represent files, but you can also build your own class extending ParseFileBase and provide a custom `ParseFileConstructor` similar to the `SubClasses`. - -Have a look at the example application for a small (non web) example. - - -```dart -//A short example for showing an image from a ParseFileBase -Widget buildImage(ParseFileBase image){ - return FutureBuilder( - future: image.download(), - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (kIsWeb) { - return Image.memory((snapshot.data as ParseWebFile).file); - } else { - return Image.file((snapshot.data as ParseFile).file); - } - } else { - return CircularProgressIndicator(); - } - }, - ); -} -``` -```dart -//A short example for storing a selected picture -//libraries: image_picker (https://pub.dev/packages/image_picker), image_picker_for_web (https://pub.dev/packages/image_picker_for_web) -PickedFile pickedFile = await ImagePicker().getImage(source: ImageSource.gallery); -ParseFileBase parseFile; -if (kIsWeb) { - //Seems weird, but this lets you get the data from the selected file as an Uint8List very easily. - ParseWebFile file = ParseWebFile(null, name: null, url: pickedFile.path); - await file.download(); - parseFile = ParseWebFile(file.file, name: file.name); -} else { - parseFile = ParseFile(File(pickedFile.path)); -} -someParseObject.set("image", parseFile); -//This saves the ParseObject as well as all of its children, and the ParseFileBase is such a child. -await someParseObject.save(); -``` - -## Other Features of this library -Main: -* Installation (View the example application) -* GeoPoints (View the example application) -* Persistent storage -* Debug Mode - Logging API calls -* Manage Session ID's tokens - -User: -* Queries -* Anonymous (View the example application) -* 3rd Party Authentication - -Objects: -* Create new object -* Extend Parse Object and create local objects that can be saved and retreived -* Queries - -## Author:- +### Author:- This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com diff --git a/packages/dart/README.md b/packages/dart/README.md new file mode 100644 index 000000000..04ff714f3 --- /dev/null +++ b/packages/dart/README.md @@ -0,0 +1,846 @@ +![Dart Logo](https://dart.dev/assets/shared/dart-logo-for-shares.png?2) ![Parse Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) + + +**THIS README IS WORK IN PROGRESS** + +## Parse For Dart! +This is a Dart package that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). + +This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) + +## Join in! +Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) + +## Getting Started +To install, either add to your pubspec.yaml +```yml +dependencies: + parse_server_sdk: ^1.0.28 +``` +or clone this repository and add to your project. As this is an early development with multiple contributors, it is probably best to download/clone and keep updating as an when a new feature is added. + + +Once you have the library added to your project, upon first call to your app (Similar to what your application class would be) add the following... + +```dart +await Parse().initialize( + keyApplicationId, + keyParseServerUrl, + ); +``` + +If you want to use secure storage or use the Flutter web/desktop SDK, please change to the below instance of CoreStorage as it has no dependencies on Flutter. + +**The `CoreStoreSembastImp` does not encrypt the data on web!** (Web is not safe anyway. Encrypt fields manually as needed.) +```dart + +await Parse().initialize( + keyParseApplicationId, + keyParseServerUrl, + coreStore: await CoreStoreSembastImp.getInstance()); +``` +It's possible to add other parameters to work with your instance of Parse Server:- + +```dart + await Parse().initialize( + keyApplicationId, + keyParseServerUrl, + masterKey: keyParseMasterKey, // Required for Back4App and others + clientKey: keyParseClientKey, // Required for some setups + debug: true, // When enabled, prints logs to console + liveQueryUrl: keyLiveQueryUrl, // Required if using LiveQuery + autoSendSessionId: true, // Required for authentication and ACL + securityContext: securityContext, // Again, required for some setups + coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB +``` + + +#### Early Web support +Currently this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. +When running directly via docker, set the env var `PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id`. +When running via express, set [ParseServerOptions](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `allowHeaders: ['X-Parse-Installation-Id']`. + +Be aware that for web ParseInstallation does include app name, version or package identifier. + + +## Objects +You can create custom objects by calling: +```dart +var dietPlan = ParseObject('DietPlan') + ..set('Name', 'Ketogenic') + ..set('Fat', 65); +await dietPlan.save(); +``` +Or update existing object by its objectId by calling: +```dart +var dietPlan = ParseObject('DietPlan') + ..objectId = 'R5EonpUDWy' + ..set('Fat', 70); +await dietPlan.save(); +``` +Verify that the object has been successfully saved using +```dart +var response = await dietPlan.save(); +if (response.success) { + dietPlan = response.results.first; +} +``` +Types supported: + * String + * Double + * Int + * Boolean + * DateTime + * File + * Geopoint + * ParseObject/ParseUser (Pointer) + * Map + * List (all types supported) + +You then have the ability to do the following with that object: +The features available are:- + * Get + * GetAll + * Create + * Save + * Query - By object Id + * Delete + * Complex queries as shown above + * Pin + * Plenty more + * Counters + * Array Operators + +## Custom Objects +You can create your own `ParseObjects` or convert your existing objects into Parse Objects by doing the following: + +```dart +class DietPlan extends ParseObject implements ParseCloneable { + + DietPlan() : super(_keyTableName); + DietPlan.clone(): this(); + + /// Looks strangely hacky but due to Flutter not using reflection, we have to + /// mimic a clone + @override clone(Map map) => DietPlan.clone()..fromJson(map); + + static const String _keyTableName = 'Diet_Plans'; + static const String keyName = 'Name'; + + String get name => get(keyName); + set name(String name) => set(keyName, name); +} + +``` + +When receiving an `ParseObject` from the SDK, you can often provide an instance of your custom object as an copy object. +To always use your custom object class, you can register your subclass at the initialization of the SDK. +```dart +Parse().initialize( + ..., + registeredSubClassMap: { + 'Diet_Plans': () => DietPlan(), + }, + parseUserConstructor: (username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress), +); +``` +Additionally you can register `SubClasses` after the initialization of the SDK. +```dart +ParseCoreData().registerSubClass('Diet_Plans', () => DietPlan()); +ParseCoreData().registerUserSubClass((username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress)); +``` +Providing a `ParseObject` as described above should still work, even if you have registered a different `SubClass`. + +For custom file classes have a lock at [here](#File). + +## Add new values to objects +To add a variable to an object call and retrieve it, call + +```dart +dietPlan.set('RandomInt', 8); +var randomInt = dietPlan.get('RandomInt'); +``` + +## Save objects using pins +You can now save an object by calling `.pin()` on an instance of an object + +```dart +dietPlan.pin(); +``` + +and to retrieve it + +```dart +var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT'); +``` + +## Storage +We now have 2 types of storage, secure and unsecure. We currently rely on 2 third party options: + +- SharedPreferences +- Sembast +Sembast offers secured storage, whilst SharePreferences wraps NSUserDefaults (on iOS) and SharedPreferences (on Android). + +The storage method is defined in the parameter __coreStore__ in Parse().initialize + +Check sample code for options + +## Increment Counter values in objects +Retrieve it, call + +```dart +var response = await dietPlan.increment("count", 1); + +``` +or using with save function + +```dart +dietPlan.setIncrement('count', 1); +dietPlan.setDecrement('count', 1); +var response = dietPlan.save() + +``` + +## Array Operator in objects +Retrieve it, call + +```dart +var response = await dietPlan.add("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.remove("listKeywords", ["a"]); + +``` +or using with save function + +```dart +dietPlan.setAdd('listKeywords', ['a','a','d']); +dietPlan.setAddUnique('listKeywords', ['a','a','d']); +dietPlan.setRemove('listKeywords', ['a']); +var response = dietPlan.save() +``` + +## Queries +Once you have setup the project and initialised the instance, you can then retreive data from your server by calling: +```dart +var apiResponse = await ParseObject('ParseTableName').getAll(); + +if (apiResponse.success){ + for (var testObject in apiResponse.result) { + print(ApplicationConstants.APP_NAME + ": " + testObject.toString()); + } +} +``` +Or you can get an object by its objectId: + +```dart +var dietPlan = await DietPlan().getObject('R5EonpUDWy'); + +if (dietPlan.success) { + print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message); +} +``` + +## Complex queries +You can create complex queries to really put your database to the test: + +```dart +var queryBuilder = QueryBuilder(DietPlan()) + ..startsWith(DietPlan.keyName, "Keto") + ..greaterThan(DietPlan.keyFat, 64) + ..lessThan(DietPlan.keyFat, 66) + ..equals(DietPlan.keyCarbs, 5); + +var response = await queryBuilder.query(); + +if (response.success) { + print(ApplicationConstants.keyAppName + ": " + ((response.results as List).first as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + response.exception.message); +} +``` + +if you want to find objects that match one of several queries, you can use __QueryBuilder.or__ method to construct a query that is an OR of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do: +```dart +ParseObject playerObject = ParseObject("Player"); + +QueryBuilder lotsOfWins = + QueryBuilder(playerObject)) + ..whereGreaterThan('wins', 50); + +QueryBuilder fewWins = + QueryBuilder(playerObject) + ..whereLessThan('wins', 5); + +QueryBuilder mainQuery = QueryBuilder.or( + playerObject, + [lotsOfWins, fewWins], + ); + +var apiResponse = await mainQuery.query(); +``` + +The features available are:- + * Equals + * Contains + * LessThan + * LessThanOrEqualTo + * GreaterThan + * GreaterThanOrEqualTo + * NotEqualTo + * StartsWith + * EndsWith + * Exists + * Near + * WithinMiles + * WithinKilometers + * WithinRadians + * WithinGeoBox + * MatchesQuery + * DoesNotMatchQuery + * MatchesKeyInQuery + * DoesNotMatchKeyInQuery + * Regex + * Order + * Limit + * Skip + * Ascending + * Descending + * Plenty more! + +## Relational queries +If you want to retrieve objects where a field contains an object that matches another query, you can use the +__whereMatchesQuery__ condition. +For example, imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts with images by doing: + +```dart +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereMatchesQuery('post', queryPost); + +var apiResponse = await queryComment.query(); +``` + +If you want to retrieve objects where a field contains an object that does not match another query, you can use the +__whereDoesNotMatchQuery__ condition. +Imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts without images by doing: + +```dart +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereDoesNotMatchQuery('post', queryPost); + +var apiResponse = await queryComment.query(); +``` + +You can use the __whereMatchesKeyInQuery__ method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:: + +```dart +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder userQuery = + QueryBuilderParseUser.forQuery()) + ..whereMatchesKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await userQuery.query(); +``` + +Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use __whereDoesNotMatchKeyInQuery__. For example, to find users whose hometown teams have losing records: + +```dart +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder losingUserQuery = + QueryBuilderParseUser.forQuery()) + ..whereDoesNotMatchKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await losingUserQuery.query(); +``` + +To filter rows based on objectId’s from pointers in a second table, you can use dot notation: +```dart +QueryBuilder rolesOfTypeX = + QueryBuilder(ParseObject('Role')) + ..whereEqualTo('type', 'x'); + +QueryBuilder groupsWithRoleX = + QueryBuilder(ParseObject('Group'))) + ..whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); + +var apiResponse = await groupsWithRoleX.query(); +``` + +## Counting Objects +If you only care about the number of games played by a particular player: + +```dart +QueryBuilder queryPlayers = + QueryBuilder(ParseObject('GameScore')) + ..whereEqualTo('playerName', 'Jonathan Walsh'); +var apiResponse = await queryPlayers.count(); +if (apiResponse.success && apiResponse.result != null) { + int countGames = apiResponse.count; +} +``` + +## Live Queries +This tool allows you to subscribe to a QueryBuilder you are interested in. Once subscribed, the server will notify clients +whenever a ParseObject that matches the QueryBuilder is created or updated, in real-time. + +Parse LiveQuery contains two parts, the LiveQuery server and the LiveQuery clients. In order to use live queries, you need +to set up both of them. + +The Parse Server configuration guide on the server is found here https://docs.parseplatform.org/parse-server/guide/#live-queries and is not part of this documentation. + +Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize: +```dart +Parse().initialize( + keyApplicationId, + keyParseServerUrl, + clientKey: keyParseClientKey, + debug: true, + liveQueryUrl: keyLiveQueryUrl, + autoSendSessionId: true); +``` + +Declare LiveQuery: +```dart +final LiveQuery liveQuery = LiveQuery(); +``` + +Set the QueryBuilder that will be monitored by LiveQuery: +```dart +QueryBuilder query = + QueryBuilder(ParseObject('TestAPI')) + ..whereEqualTo('intNumber', 1); +``` +__Create a subscription__ +You’ll get the LiveQuery events through this subscription. +The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you. + +```dart +Subscription subscription = await liveQuery.client.subscribe(query); +``` + +__Event Handling__ +We define several types of events you’ll get through a subscription object: + +__Create event__ +When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. +The object is the ParseObject which was created. +```dart +subscription.on(LiveQueryEvent.create, (value) { + print('*** CREATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Update event__ +When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the +QueryBuilder before and after changes), you’ll get this event. +The object is the ParseObject which was updated. Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.update, (value) { + print('*** UPDATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Enter event__ +When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, +you’ll get this event. The object is the ParseObject which enters the QueryBuilder. +Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.enter, (value) { + print('*** ENTER ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Leave event__ +When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, +you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. +Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.leave, (value) { + print('*** LEAVE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Delete event__ +When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. +The object is the ParseObject which is deleted +```dart +subscription.on(LiveQueryEvent.delete, (value) { + print('*** DELETE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Unsubscribe__ +If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. +After that, you won’t get any events from the subscription object and will close the WebSocket connection to the +LiveQuery server. + +```dart +liveQuery.client.unSubscribe(subscription); +``` + +__Disconnection__ +In case the client's connection to the server breaks, +LiveQuery will automatically try to reconnect. +LiveQuery will wait at increasing intervals between reconnection attempts. +By default, these intervals are set to `[0, 500, 1000, 2000, 5000, 10000]` for mobile and `[0, 500, 1000, 2000, 5000]` for web. +You can change these by providing a custom list using the `liveListRetryIntervals` parameter at `Parse.initialize()` ("-1" means "do not try to reconnect"). + +## ParseLiveList +ParseLiveList makes implementing a dynamic List as simple as possible. + +### General Use +It ships with the ParseLiveList class itself, this class manages all elements of the list, sorts them, +keeps itself up to date and Notifies you on changes. + +### included Sub-Objects +By default, ParseLiveQuery will provide you with all the objects you included in your Query like this: +```dart +queryBuilder.includeObject(/*List of all the included sub-objects*/); +``` +ParseLiveList will not listen for updates on this objects by default. +To activate listening for updates on all included objects, add `listenOnAllSubItems: true` to your ParseLiveListWidgets constructor. +If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. +Just as QueryBuilder, ParseLiveList supports nested sub-objects too. + +**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. + +## Users +You can create and control users just as normal using this SDK. + +To register a user, first create one : +```dart +var user = ParseUser().create("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com"); +``` +Then have the user sign up: + +```dart +var response = await user.signUp(); +if (response.success) user = response.result; +``` +You can also login with the user: +```dart +var response = await user.login(); +if (response.success) user = response.result; +``` +You can also logout with the user: +```dart +var response = await user.logout(); +if (response.success) { + print('User logout'); +} +``` +Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init() on startup to check for a logged in user. +```dart +user = ParseUser.currentUser(); +``` + +To add additional columns to the user: +```dart +var user = ParseUser("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com") + ..set("userLocation", "FlutterLand"); +``` + +Other user features are:- + * Request Password Reset + * Verification Email Request + * Get all users + * Save + * Destroy user + * Queries + + ## Facebook, OAuth and 3rd Party Login/User + + Usually, each provider will provide their own library for logins, but the loginWith method on ParseUser accepts a name of provider, then a Map with the authentication details required. + For Facebook and the example below, we used the library provided at https://pub.dev/packages/flutter_facebook_login + + ``` + Future goToFacebookLogin() async { + final FacebookLogin facebookLogin = FacebookLogin(); + final FacebookLoginResult result = await facebookLogin.logInWithReadPermissions(['email']); + + switch (result.status) { + case FacebookLoginStatus.loggedIn: + final ParseResponse response = await ParseUser.loginWith( + 'facebook', + facebook(result.accessToken.token, + result.accessToken.userId, + result.accessToken.expires)); + + if (response.success) { + // User is logged in, test with ParseUser.currentUser() + } + break; + case FacebookLoginStatus.cancelledByUser: + // User cancelled + break; + case FacebookLoginStatus.error: + // Error + break; + } + } +``` + +For Google and the example below, we used the library provided at https://pub.dev/packages/google_sign_in + +``` +class OAuthLogin { + final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'https://www.googleapis.com/auth/contacts.readonly'] ); + + sigInGoogle() async { + GoogleSignInAccount account = await _googleSignIn.signIn(); + GoogleSignInAuthentication authentication = await account.authentication; + await ParseUser.loginWith( + 'google', + google(_googleSignIn.currentUser.id, + authentication.accessToken, + authentication.idToken)); + } +} +``` + +## Security for Objects - ParseACL +For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. +To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. + +If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write. +The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. +To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that +limits access to that user. An object’s ACL is updated when the object is saved, like any other property. + +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(owner: user); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using +__setReadAccess__ and __setWriteAccess__ +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(); +//grant total access to current user +parseACL.setReadAccess(userId: user.objectId, allowed: true); +parseACL.setWriteAccess(userId: user.objectId, allowed: true); +//grant read access to userId: 'TjRuDjuSAO' +parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true); +parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess. +```dart +ParseACL parseACL = ParseACL(); +parseACL.setPublicReadAccess(allowed: true); +parseACL.setPublicWriteAccess(allowed: true); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Operations that are forbidden, such as deleting an object that you do not have write access to, result in a +ParseError with code 101: 'ObjectNotFound'. +For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which +object ids do not exist at all. + +You can retrieve the ACL list of an object using: +```dart +ParseACL parseACL = parseObject.getACL(); +``` + +## Config +The SDK supports Parse Config. A map of all configs can be grabbed from the server by calling : +```dart +var response = await ParseConfig().getConfigs(); +``` + +and to add a config: +```dart +ParseConfig().addConfig('TestConfig', 'testing'); +``` + +## Cloud Functions +The SDK supports call Cloud Functions. + +Executes a cloud function that returns a ParseObject type +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final ParseResponse result = + await function.executeObjectFunction(); +if (result.success) { + if (result.result is ParseObject) { + final ParseObject parseObject = result.result; + print(parseObject.className); + } +} +``` + +Executes a cloud function with parameters +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final Map params = {'plan': 'paid'}; +function.execute(parameters: params); +``` + +## Relation + +The SDK supports Relation. + +To add relation to object: + +```dart +dietPlan.addRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); +``` + +To remove relation to object: + +```dart +dietPlan.removeRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); +``` + +To Retrive a relation instance for user, call: +```dart +final relation = dietPlan.getRelation('fruits'); +``` + +and then you can add a relation to the passed in object: +``` +relation.add(dietPlan); +final result = await user.save(); +``` + +To retrieve objects that are members of Relation field of a parent object: +```dart +QueryBuilder query = + QueryBuilder(ParseObject('Fruits')) + ..whereRelatedTo('fruits', 'DietPlan', DietPlan.objectId); +``` + +## File +There are three different file classes in this SDK: +- `ParseFileBase` is and abstract class and is the foundation of every file class that can be handled by this SDK. +- `ParseFile` (former the only file class in the SDK) extends ParseFileBase and is by default used as the file class on every platform but web. +This class uses a `File` from `dart:io` for storing the raw file. +- `ParseWebFile` is the equivalent to ParseFile used at Flutter Web. +This class uses an `Uint8List` for storing the raw file. + +These classes are used by default to represent files, but you can also build your own class extending ParseFileBase and provide a custom `ParseFileConstructor` similar to the `SubClasses`. + +Have a look at the example application for a small (non web) example. + + +```dart +//A short example for showing an image from a ParseFileBase +Widget buildImage(ParseFileBase image){ + return FutureBuilder( + future: image.download(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (kIsWeb) { + return Image.memory((snapshot.data as ParseWebFile).file); + } else { + return Image.file((snapshot.data as ParseFile).file); + } + } else { + return CircularProgressIndicator(); + } + }, + ); +} +``` +```dart +//A short example for storing a selected picture +//libraries: image_picker (https://pub.dev/packages/image_picker), image_picker_for_web (https://pub.dev/packages/image_picker_for_web) +PickedFile pickedFile = await ImagePicker().getImage(source: ImageSource.gallery); +ParseFileBase parseFile; +if (kIsWeb) { + //Seems weird, but this lets you get the data from the selected file as an Uint8List very easily. + ParseWebFile file = ParseWebFile(null, name: null, url: pickedFile.path); + await file.download(); + parseFile = ParseWebFile(file.file, name: file.name); +} else { + parseFile = ParseFile(File(pickedFile.path)); +} +someParseObject.set("image", parseFile); +//This saves the ParseObject as well as all of its children, and the ParseFileBase is such a child. +await someParseObject.save(); +``` + +## Other Features of this library +Main: +* Installation (View the example application) +* GeoPoints (View the example application) +* Persistent storage +* Debug Mode - Logging API calls +* Manage Session ID's tokens + +User: +* Queries +* Anonymous (View the example application) +* 3rd Party Authentication + +Objects: +* Create new object +* Extend Parse Object and create local objects that can be saved and retreived +* Queries + +## Author:- +This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com diff --git a/lib/parse_server_sdk_dart.dart b/packages/dart/lib/parse_server_sdk.dart similarity index 100% rename from lib/parse_server_sdk_dart.dart rename to packages/dart/lib/parse_server_sdk.dart diff --git a/lib/src/base/parse_constants.dart b/packages/dart/lib/src/base/parse_constants.dart similarity index 100% rename from lib/src/base/parse_constants.dart rename to packages/dart/lib/src/base/parse_constants.dart diff --git a/lib/src/data/app_info.dart b/packages/dart/lib/src/data/app_info.dart similarity index 100% rename from lib/src/data/app_info.dart rename to packages/dart/lib/src/data/app_info.dart diff --git a/lib/src/data/core_store.dart b/packages/dart/lib/src/data/core_store.dart similarity index 100% rename from lib/src/data/core_store.dart rename to packages/dart/lib/src/data/core_store.dart diff --git a/lib/src/data/parse_core_data.dart b/packages/dart/lib/src/data/parse_core_data.dart similarity index 100% rename from lib/src/data/parse_core_data.dart rename to packages/dart/lib/src/data/parse_core_data.dart diff --git a/lib/src/data/parse_subclass_handler.dart b/packages/dart/lib/src/data/parse_subclass_handler.dart similarity index 100% rename from lib/src/data/parse_subclass_handler.dart rename to packages/dart/lib/src/data/parse_subclass_handler.dart diff --git a/lib/src/enums/parse_enum_api_rq.dart b/packages/dart/lib/src/enums/parse_enum_api_rq.dart similarity index 100% rename from lib/src/enums/parse_enum_api_rq.dart rename to packages/dart/lib/src/enums/parse_enum_api_rq.dart diff --git a/lib/src/network/parse_connectivity.dart b/packages/dart/lib/src/network/parse_connectivity.dart similarity index 100% rename from lib/src/network/parse_connectivity.dart rename to packages/dart/lib/src/network/parse_connectivity.dart diff --git a/lib/src/network/parse_http_client.dart b/packages/dart/lib/src/network/parse_http_client.dart similarity index 100% rename from lib/src/network/parse_http_client.dart rename to packages/dart/lib/src/network/parse_http_client.dart diff --git a/lib/src/network/parse_live_query.dart b/packages/dart/lib/src/network/parse_live_query.dart similarity index 100% rename from lib/src/network/parse_live_query.dart rename to packages/dart/lib/src/network/parse_live_query.dart diff --git a/lib/src/network/parse_query.dart b/packages/dart/lib/src/network/parse_query.dart similarity index 100% rename from lib/src/network/parse_query.dart rename to packages/dart/lib/src/network/parse_query.dart diff --git a/lib/src/network/parse_websocket.dart b/packages/dart/lib/src/network/parse_websocket.dart similarity index 100% rename from lib/src/network/parse_websocket.dart rename to packages/dart/lib/src/network/parse_websocket.dart diff --git a/lib/src/network/parse_websocket_html.dart b/packages/dart/lib/src/network/parse_websocket_html.dart similarity index 100% rename from lib/src/network/parse_websocket_html.dart rename to packages/dart/lib/src/network/parse_websocket_html.dart diff --git a/lib/src/network/parse_websocket_io.dart b/packages/dart/lib/src/network/parse_websocket_io.dart similarity index 100% rename from lib/src/network/parse_websocket_io.dart rename to packages/dart/lib/src/network/parse_websocket_io.dart diff --git a/lib/src/objects/parse_acl.dart b/packages/dart/lib/src/objects/parse_acl.dart similarity index 100% rename from lib/src/objects/parse_acl.dart rename to packages/dart/lib/src/objects/parse_acl.dart diff --git a/lib/src/objects/parse_base.dart b/packages/dart/lib/src/objects/parse_base.dart similarity index 100% rename from lib/src/objects/parse_base.dart rename to packages/dart/lib/src/objects/parse_base.dart diff --git a/lib/src/objects/parse_cloneable.dart b/packages/dart/lib/src/objects/parse_cloneable.dart similarity index 100% rename from lib/src/objects/parse_cloneable.dart rename to packages/dart/lib/src/objects/parse_cloneable.dart diff --git a/lib/src/objects/parse_config.dart b/packages/dart/lib/src/objects/parse_config.dart similarity index 100% rename from lib/src/objects/parse_config.dart rename to packages/dart/lib/src/objects/parse_config.dart diff --git a/lib/src/objects/parse_error.dart b/packages/dart/lib/src/objects/parse_error.dart similarity index 100% rename from lib/src/objects/parse_error.dart rename to packages/dart/lib/src/objects/parse_error.dart diff --git a/lib/src/objects/parse_file.dart b/packages/dart/lib/src/objects/parse_file.dart similarity index 100% rename from lib/src/objects/parse_file.dart rename to packages/dart/lib/src/objects/parse_file.dart diff --git a/lib/src/objects/parse_file_base.dart b/packages/dart/lib/src/objects/parse_file_base.dart similarity index 100% rename from lib/src/objects/parse_file_base.dart rename to packages/dart/lib/src/objects/parse_file_base.dart diff --git a/lib/src/objects/parse_file_web.dart b/packages/dart/lib/src/objects/parse_file_web.dart similarity index 100% rename from lib/src/objects/parse_file_web.dart rename to packages/dart/lib/src/objects/parse_file_web.dart diff --git a/lib/src/objects/parse_function.dart b/packages/dart/lib/src/objects/parse_function.dart similarity index 100% rename from lib/src/objects/parse_function.dart rename to packages/dart/lib/src/objects/parse_function.dart diff --git a/lib/src/objects/parse_geo_point.dart b/packages/dart/lib/src/objects/parse_geo_point.dart similarity index 100% rename from lib/src/objects/parse_geo_point.dart rename to packages/dart/lib/src/objects/parse_geo_point.dart diff --git a/lib/src/objects/parse_installation.dart b/packages/dart/lib/src/objects/parse_installation.dart similarity index 100% rename from lib/src/objects/parse_installation.dart rename to packages/dart/lib/src/objects/parse_installation.dart diff --git a/lib/src/objects/parse_merge.dart b/packages/dart/lib/src/objects/parse_merge.dart similarity index 100% rename from lib/src/objects/parse_merge.dart rename to packages/dart/lib/src/objects/parse_merge.dart diff --git a/lib/src/objects/parse_object.dart b/packages/dart/lib/src/objects/parse_object.dart similarity index 100% rename from lib/src/objects/parse_object.dart rename to packages/dart/lib/src/objects/parse_object.dart diff --git a/lib/src/objects/parse_relation.dart b/packages/dart/lib/src/objects/parse_relation.dart similarity index 100% rename from lib/src/objects/parse_relation.dart rename to packages/dart/lib/src/objects/parse_relation.dart diff --git a/lib/src/objects/parse_response.dart b/packages/dart/lib/src/objects/parse_response.dart similarity index 100% rename from lib/src/objects/parse_response.dart rename to packages/dart/lib/src/objects/parse_response.dart diff --git a/lib/src/objects/parse_session.dart b/packages/dart/lib/src/objects/parse_session.dart similarity index 100% rename from lib/src/objects/parse_session.dart rename to packages/dart/lib/src/objects/parse_session.dart diff --git a/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart similarity index 100% rename from lib/src/objects/parse_user.dart rename to packages/dart/lib/src/objects/parse_user.dart diff --git a/lib/src/objects/response/parse_error_response.dart b/packages/dart/lib/src/objects/response/parse_error_response.dart similarity index 100% rename from lib/src/objects/response/parse_error_response.dart rename to packages/dart/lib/src/objects/response/parse_error_response.dart diff --git a/lib/src/objects/response/parse_exception_response.dart b/packages/dart/lib/src/objects/response/parse_exception_response.dart similarity index 100% rename from lib/src/objects/response/parse_exception_response.dart rename to packages/dart/lib/src/objects/response/parse_exception_response.dart diff --git a/lib/src/objects/response/parse_response_builder.dart b/packages/dart/lib/src/objects/response/parse_response_builder.dart similarity index 100% rename from lib/src/objects/response/parse_response_builder.dart rename to packages/dart/lib/src/objects/response/parse_response_builder.dart diff --git a/lib/src/objects/response/parse_response_utils.dart b/packages/dart/lib/src/objects/response/parse_response_utils.dart similarity index 100% rename from lib/src/objects/response/parse_response_utils.dart rename to packages/dart/lib/src/objects/response/parse_response_utils.dart diff --git a/lib/src/objects/response/parse_success_no_results.dart b/packages/dart/lib/src/objects/response/parse_success_no_results.dart similarity index 100% rename from lib/src/objects/response/parse_success_no_results.dart rename to packages/dart/lib/src/objects/response/parse_success_no_results.dart diff --git a/lib/src/storage/core_store.dart b/packages/dart/lib/src/storage/core_store.dart similarity index 100% rename from lib/src/storage/core_store.dart rename to packages/dart/lib/src/storage/core_store.dart diff --git a/lib/src/storage/core_store_sem_impl.dart b/packages/dart/lib/src/storage/core_store_sem_impl.dart similarity index 100% rename from lib/src/storage/core_store_sem_impl.dart rename to packages/dart/lib/src/storage/core_store_sem_impl.dart diff --git a/lib/src/storage/xxtea_codec.dart b/packages/dart/lib/src/storage/xxtea_codec.dart similarity index 100% rename from lib/src/storage/xxtea_codec.dart rename to packages/dart/lib/src/storage/xxtea_codec.dart diff --git a/lib/src/utils/parse_date_format.dart b/packages/dart/lib/src/utils/parse_date_format.dart similarity index 100% rename from lib/src/utils/parse_date_format.dart rename to packages/dart/lib/src/utils/parse_date_format.dart diff --git a/lib/src/utils/parse_decoder.dart b/packages/dart/lib/src/utils/parse_decoder.dart similarity index 100% rename from lib/src/utils/parse_decoder.dart rename to packages/dart/lib/src/utils/parse_decoder.dart diff --git a/lib/src/utils/parse_encoder.dart b/packages/dart/lib/src/utils/parse_encoder.dart similarity index 100% rename from lib/src/utils/parse_encoder.dart rename to packages/dart/lib/src/utils/parse_encoder.dart diff --git a/lib/src/utils/parse_file_extensions.dart b/packages/dart/lib/src/utils/parse_file_extensions.dart similarity index 100% rename from lib/src/utils/parse_file_extensions.dart rename to packages/dart/lib/src/utils/parse_file_extensions.dart diff --git a/lib/src/utils/parse_live_list.dart b/packages/dart/lib/src/utils/parse_live_list.dart similarity index 100% rename from lib/src/utils/parse_live_list.dart rename to packages/dart/lib/src/utils/parse_live_list.dart diff --git a/lib/src/utils/parse_logger.dart b/packages/dart/lib/src/utils/parse_logger.dart similarity index 100% rename from lib/src/utils/parse_logger.dart rename to packages/dart/lib/src/utils/parse_logger.dart diff --git a/lib/src/utils/parse_login_helpers.dart b/packages/dart/lib/src/utils/parse_login_helpers.dart similarity index 100% rename from lib/src/utils/parse_login_helpers.dart rename to packages/dart/lib/src/utils/parse_login_helpers.dart diff --git a/lib/src/utils/parse_utils.dart b/packages/dart/lib/src/utils/parse_utils.dart similarity index 100% rename from lib/src/utils/parse_utils.dart rename to packages/dart/lib/src/utils/parse_utils.dart diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml new file mode 100644 index 000000000..888de316e --- /dev/null +++ b/packages/dart/pubspec.yaml @@ -0,0 +1,29 @@ +name: parse_server_sdk +description: Dart plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) +version: 1.0.28 +homepage: https://github.com/phillwiggins/flutter_parse_sdk + +environment: + sdk: ">=2.2.2 <3.0.0" + +dependencies: + http: ^0.12.2 + + # Networking + web_socket_channel: ^1.1.0 + + #Database + sembast: ^2.4.7+6 + sembast_web: '>=1.0.0' + xxtea: ^2.0.3 + + # Utils + uuid: ^2.2.2 + meta: ^1.1.8 + path: ^1.7.0 + +dev_dependencies: + # Testing + flutter_test: + sdk: flutter + mockito: ^4.1.1 diff --git a/packages/flutter/README.md b/packages/flutter/README.md new file mode 100644 index 000000000..800477861 --- /dev/null +++ b/packages/flutter/README.md @@ -0,0 +1,893 @@ + +![Parse Logo](https://upload.wikimedia.org/wikipedia/commons/1/17/Google-flutter-logo.png)![Flutter Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) + +## Parse For Flutter! +Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). + +This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) + +## Join in! +Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) + +## Getting Started +To install, either add to your pubspec.yaml +```yml +dependencies: + parse_server_sdk: ^1.0.28 +``` +or clone this repository and add to your project. As this is an early development with multiple contributors, it is probably best to download/clone and keep updating as an when a new feature is added. + + +Once you have the library added to your project, upon first call to your app (Similar to what your application class would be) add the following... + +```dart +await Parse().initialize( + keyApplicationId, + keyParseServerUrl); +``` + +If you want to use secure storage or use the Flutter web/desktop SDK, please change to the below instance of CoreStorage as it has no dependencies on Flutter. + +**The `CoreStoreSembastImp` does not encrypt the data on web!** (Web is not safe anyway. Encrypt fields manually as needed.) +```dart + +await Parse().initialize( + keyParseApplicationId, + keyParseServerUrl, + coreStore: await CoreStoreSembastImp.getInstance()); +``` +It's possible to add other parameters to work with your instance of Parse Server:- + +```dart + await Parse().initialize( + keyApplicationId, + keyParseServerUrl, + masterKey: keyParseMasterKey, // Required for Back4App and others + clientKey: keyParseClientKey, // Required for some setups + debug: true, // When enabled, prints logs to console + liveQueryUrl: keyLiveQueryUrl, // Required if using LiveQuery + autoSendSessionId: true, // Required for authentication and ACL + securityContext: securityContext, // Again, required for some setups + coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB +``` + + +#### Early Web support +Currently this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. +When running directly via docker, set the env var `PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id`. +When running via express, set [ParseServerOptions](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `allowHeaders: ['X-Parse-Installation-Id']`. + +Be aware that for web ParseInstallation does include app name, version or package identifier. + + +## Objects +You can create custom objects by calling: +```dart +var dietPlan = ParseObject('DietPlan') + ..set('Name', 'Ketogenic') + ..set('Fat', 65); +await dietPlan.save(); +``` +Or update existing object by its objectId by calling: +```dart +var dietPlan = ParseObject('DietPlan') + ..objectId = 'R5EonpUDWy' + ..set('Fat', 70); +await dietPlan.save(); +``` +Verify that the object has been successfully saved using +```dart +var response = await dietPlan.save(); +if (response.success) { + dietPlan = response.results.first; +} +``` +Types supported: + * String + * Double + * Int + * Boolean + * DateTime + * File + * Geopoint + * ParseObject/ParseUser (Pointer) + * Map + * List (all types supported) + +You then have the ability to do the following with that object: +The features available are:- + * Get + * GetAll + * Create + * Save + * Query - By object Id + * Delete + * Complex queries as shown above + * Pin + * Plenty more + * Counters + * Array Operators + +## Custom Objects +You can create your own `ParseObjects` or convert your existing objects into Parse Objects by doing the following: + +```dart +class DietPlan extends ParseObject implements ParseCloneable { + + DietPlan() : super(_keyTableName); + DietPlan.clone(): this(); + + /// Looks strangely hacky but due to Flutter not using reflection, we have to + /// mimic a clone + @override clone(Map map) => DietPlan.clone()..fromJson(map); + + static const String _keyTableName = 'Diet_Plans'; + static const String keyName = 'Name'; + + String get name => get(keyName); + set name(String name) => set(keyName, name); +} + +``` + +When receiving an `ParseObject` from the SDK, you can often provide an instance of your custom object as an copy object. +To always use your custom object class, you can register your subclass at the initialization of the SDK. +```dart +Parse().initialize( + ..., + registeredSubClassMap: { + 'Diet_Plans': () => DietPlan(), + }, + parseUserConstructor: (username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress), +); +``` +Additionally you can register `SubClasses` after the initialization of the SDK. +```dart +ParseCoreData().registerSubClass('Diet_Plans', () => DietPlan()); +ParseCoreData().registerUserSubClass((username, password, emailAddress, {client, debug, sessionToken}) => CustomParseUser(username, password, emailAddress)); +``` +Providing a `ParseObject` as described above should still work, even if you have registered a different `SubClass`. + +For custom file classes have a lock at [here](#File). + +## Add new values to objects +To add a variable to an object call and retrieve it, call + +```dart +dietPlan.set('RandomInt', 8); +var randomInt = dietPlan.get('RandomInt'); +``` + +## Save objects using pins +You can now save an object by calling `.pin()` on an instance of an object + +```dart +dietPlan.pin(); +``` + +and to retrieve it + +```dart +var dietPlan = DietPlan().fromPin('OBJECT ID OF OBJECT'); +``` + +## Storage +We now have 2 types of storage, secure and unsecure. We currently rely on 2 third party options: + +- SharedPreferences +- Sembast +Sembast offers secured storage, whilst SharePreferences wraps NSUserDefaults (on iOS) and SharedPreferences (on Android). + +The storage method is defined in the parameter __coreStore__ in Parse().initialize + +Check sample code for options + +## Increment Counter values in objects +Retrieve it, call + +```dart +var response = await dietPlan.increment("count", 1); + +``` +or using with save function + +```dart +dietPlan.setIncrement('count', 1); +dietPlan.setDecrement('count', 1); +var response = dietPlan.save() + +``` + +## Array Operator in objects +Retrieve it, call + +```dart +var response = await dietPlan.add("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.addUnique("listKeywords", ["a", "a","d"]); + +var response = await dietPlan.remove("listKeywords", ["a"]); + +``` +or using with save function + +```dart +dietPlan.setAdd('listKeywords', ['a','a','d']); +dietPlan.setAddUnique('listKeywords', ['a','a','d']); +dietPlan.setRemove('listKeywords', ['a']); +var response = dietPlan.save() +``` + +## Queries +Once you have setup the project and initialised the instance, you can then retreive data from your server by calling: +```dart +var apiResponse = await ParseObject('ParseTableName').getAll(); + +if (apiResponse.success){ + for (var testObject in apiResponse.result) { + print(ApplicationConstants.APP_NAME + ": " + testObject.toString()); + } +} +``` +Or you can get an object by its objectId: + +```dart +var dietPlan = await DietPlan().getObject('R5EonpUDWy'); + +if (dietPlan.success) { + print(ApplicationConstants.keyAppName + ": " + (dietPlan.result as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + dietPlan.exception.message); +} +``` + +## Complex queries +You can create complex queries to really put your database to the test: + +```dart +var queryBuilder = QueryBuilder(DietPlan()) + ..startsWith(DietPlan.keyName, "Keto") + ..greaterThan(DietPlan.keyFat, 64) + ..lessThan(DietPlan.keyFat, 66) + ..equals(DietPlan.keyCarbs, 5); + +var response = await queryBuilder.query(); + +if (response.success) { + print(ApplicationConstants.keyAppName + ": " + ((response.results as List).first as DietPlan).toString()); +} else { + print(ApplicationConstants.keyAppName + ": " + response.exception.message); +} +``` + +if you want to find objects that match one of several queries, you can use __QueryBuilder.or__ method to construct a query that is an OR of the queries passed in. For instance if you want to find players who either have a lot of wins or a few wins, you can do: +```dart +ParseObject playerObject = ParseObject("Player"); + +QueryBuilder lotsOfWins = + QueryBuilder(playerObject)) + ..whereGreaterThan('wins', 50); + +QueryBuilder fewWins = + QueryBuilder(playerObject) + ..whereLessThan('wins', 5); + +QueryBuilder mainQuery = QueryBuilder.or( + playerObject, + [lotsOfWins, fewWins], + ); + +var apiResponse = await mainQuery.query(); +``` + +The features available are:- + * Equals + * Contains + * LessThan + * LessThanOrEqualTo + * GreaterThan + * GreaterThanOrEqualTo + * NotEqualTo + * StartsWith + * EndsWith + * Exists + * Near + * WithinMiles + * WithinKilometers + * WithinRadians + * WithinGeoBox + * MatchesQuery + * DoesNotMatchQuery + * MatchesKeyInQuery + * DoesNotMatchKeyInQuery + * Regex + * Order + * Limit + * Skip + * Ascending + * Descending + * Plenty more! + +## Relational queries +If you want to retrieve objects where a field contains an object that matches another query, you can use the +__whereMatchesQuery__ condition. +For example, imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts with images by doing: + +```dart +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereMatchesQuery('post', queryPost); + +var apiResponse = await queryComment.query(); +``` + +If you want to retrieve objects where a field contains an object that does not match another query, you can use the +__whereDoesNotMatchQuery__ condition. +Imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. +You can find comments on posts without images by doing: + +```dart +QueryBuilder queryPost = + QueryBuilder(ParseObject('Post')) + ..whereValueExists('image', true); + +QueryBuilder queryComment = + QueryBuilder(ParseObject('Comment')) + ..whereDoesNotMatchQuery('post', queryPost); + +var apiResponse = await queryComment.query(); +``` + +You can use the __whereMatchesKeyInQuery__ method to get objects where a key matches the value of a key in a set of objects resulting from another query. For example, if you have a class containing sports teams and you store a user’s hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:: + +```dart +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder userQuery = + QueryBuilderParseUser.forQuery()) + ..whereMatchesKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await userQuery.query(); +``` + +Conversely, to get objects where a key does not match the value of a key in a set of objects resulting from another query, use __whereDoesNotMatchKeyInQuery__. For example, to find users whose hometown teams have losing records: + +```dart +QueryBuilder teamQuery = + QueryBuilder(ParseObject('Team')) + ..whereGreaterThan('winPct', 0.5); + +QueryBuilder losingUserQuery = + QueryBuilderParseUser.forQuery()) + ..whereDoesNotMatchKeyInQuery('hometown', 'city', teamQuery); + +var apiResponse = await losingUserQuery.query(); +``` + +To filter rows based on objectId’s from pointers in a second table, you can use dot notation: +```dart +QueryBuilder rolesOfTypeX = + QueryBuilder(ParseObject('Role')) + ..whereEqualTo('type', 'x'); + +QueryBuilder groupsWithRoleX = + QueryBuilder(ParseObject('Group'))) + ..whereMatchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); + +var apiResponse = await groupsWithRoleX.query(); +``` + +## Counting Objects +If you only care about the number of games played by a particular player: + +```dart +QueryBuilder queryPlayers = + QueryBuilder(ParseObject('GameScore')) + ..whereEqualTo('playerName', 'Jonathan Walsh'); +var apiResponse = await queryPlayers.count(); +if (apiResponse.success && apiResponse.result != null) { + int countGames = apiResponse.count; +} +``` + +## Live Queries +This tool allows you to subscribe to a QueryBuilder you are interested in. Once subscribed, the server will notify clients +whenever a ParseObject that matches the QueryBuilder is created or updated, in real-time. + +Parse LiveQuery contains two parts, the LiveQuery server and the LiveQuery clients. In order to use live queries, you need +to set up both of them. + +The Parse Server configuration guide on the server is found here https://docs.parseplatform.org/parse-server/guide/#live-queries and is not part of this documentation. + +Initialize the Parse Live Query by entering the parameter liveQueryUrl in Parse().initialize: +```dart +Parse().initialize( + keyApplicationId, + keyParseServerUrl, + clientKey: keyParseClientKey, + debug: true, + liveQueryUrl: keyLiveQueryUrl, + autoSendSessionId: true); +``` + +Declare LiveQuery: +```dart +final LiveQuery liveQuery = LiveQuery(); +``` + +Set the QueryBuilder that will be monitored by LiveQuery: +```dart +QueryBuilder query = + QueryBuilder(ParseObject('TestAPI')) + ..whereEqualTo('intNumber', 1); +``` +__Create a subscription__ +You’ll get the LiveQuery events through this subscription. +The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you. + +```dart +Subscription subscription = await liveQuery.client.subscribe(query); +``` + +__Event Handling__ +We define several types of events you’ll get through a subscription object: + +__Create event__ +When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. +The object is the ParseObject which was created. +```dart +subscription.on(LiveQueryEvent.create, (value) { + print('*** CREATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Update event__ +When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the +QueryBuilder before and after changes), you’ll get this event. +The object is the ParseObject which was updated. Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.update, (value) { + print('*** UPDATE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Enter event__ +When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, +you’ll get this event. The object is the ParseObject which enters the QueryBuilder. +Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.enter, (value) { + print('*** ENTER ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Leave event__ +When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, +you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. +Its content is the latest value of the ParseObject. +```dart +subscription.on(LiveQueryEvent.leave, (value) { + print('*** LEAVE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Delete event__ +When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. +The object is the ParseObject which is deleted +```dart +subscription.on(LiveQueryEvent.delete, (value) { + print('*** DELETE ***: ${DateTime.now().toString()}\n $value '); + print((value as ParseObject).objectId); + print((value as ParseObject).updatedAt); + print((value as ParseObject).createdAt); + print((value as ParseObject).get('objectId')); + print((value as ParseObject).get('updatedAt')); + print((value as ParseObject).get('createdAt')); +}); +``` + +__Unsubscribe__ +If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. +After that, you won’t get any events from the subscription object and will close the WebSocket connection to the +LiveQuery server. + +```dart +liveQuery.client.unSubscribe(subscription); +``` + +__Disconnection__ +In case the client's connection to the server breaks, +LiveQuery will automatically try to reconnect. +LiveQuery will wait at increasing intervals between reconnection attempts. +By default, these intervals are set to `[0, 500, 1000, 2000, 5000, 10000]` for mobile and `[0, 500, 1000, 2000, 5000]` for web. +You can change these by providing a custom list using the `liveListRetryIntervals` parameter at `Parse.initialize()` ("-1" means "do not try to reconnect"). + +## ParseLiveList +ParseLiveList makes implementing a dynamic List as simple as possible. + +#### General Use +It ships with the ParseLiveList class itself, this class manages all elements of the list, sorts them, +keeps itself up to date and Notifies you on changes. + +ParseLiveListWidget is a widget that handles all the communication with the ParseLiveList for you. +Using ParseLiveListWidget you can create a dynamic List by just providing a QueryBuilder. + +```dart +ParseLiveListWidget( + query: query, + ); +``` +To customize the List Elements, you can provide a childBuilder. +```dart +ParseLiveListWidget( + query: query, + reverse: false, + childBuilder: + (BuildContext context, ParseLiveListElementSnapshot snapshot) { + if (snapshot.failed) { + return const Text('something went wrong!'); + } else if (snapshot.hasData) { + return ListTile( + title: Text( + snapshot.loadedData.get("text"), + ), + ); + } else { + return const ListTile( + leading: CircularProgressIndicator(), + ); + } + }, +); +``` +Similar to the standard ListView, you can provide arguments like reverse or shrinkWrap. +By providing the listLoadingElement, you can show the user something while the list is loading. +```dart +ParseLiveListWidget( + query: query, + childBuilder: childBuilder, + listLoadingElement: Center( + child: CircularProgressIndicator(), + ), +); +``` +By providing the duration argument, you can change the animation speed. +```dart +ParseLiveListWidget( + query: query, + childBuilder: childBuilder, + duration: Duration(seconds: 1), +); +``` +### included Sub-Objects +By default, ParseLiveQuery will provide you with all the objects you included in your Query like this: +```dart +queryBuilder.includeObject(/*List of all the included sub-objects*/); +``` +ParseLiveList will not listen for updates on this objects by default. +To activate listening for updates on all included objects, add `listenOnAllSubItems: true` to your ParseLiveListWidgets constructor. +If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. +Just as QueryBuilder, ParseLiveList supports nested sub-objects too. + +**NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. + +## Users +You can create and control users just as normal using this SDK. + +To register a user, first create one : +```dart +var user = ParseUser().create("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com"); +``` +Then have the user sign up: + +```dart +var response = await user.signUp(); +if (response.success) user = response.result; +``` +You can also login with the user: +```dart +var response = await user.login(); +if (response.success) user = response.result; +``` +You can also logout with the user: +```dart +var response = await user.logout(); +if (response.success) { + print('User logout'); +} +``` +Also, once logged in you can manage sessions tokens. This feature can be called after Parse().init() on startup to check for a logged in user. +```dart +user = ParseUser.currentUser(); +``` + +To add additional columns to the user: +```dart +var user = ParseUser("TestFlutter", "TestPassword123", "TestFlutterSDK@gmail.com") + ..set("userLocation", "FlutterLand"); +``` + +Other user features are:- + * Request Password Reset + * Verification Email Request + * Get all users + * Save + * Destroy user + * Queries + + ## Facebook, OAuth and 3rd Party Login/User + + Usually, each provider will provide their own library for logins, but the loginWith method on ParseUser accepts a name of provider, then a Map with the authentication details required. + For Facebook and the example below, we used the library provided at https://pub.dev/packages/flutter_facebook_login + + ``` + Future goToFacebookLogin() async { + final FacebookLogin facebookLogin = FacebookLogin(); + final FacebookLoginResult result = await facebookLogin.logInWithReadPermissions(['email']); + + switch (result.status) { + case FacebookLoginStatus.loggedIn: + final ParseResponse response = await ParseUser.loginWith( + 'facebook', + facebook(result.accessToken.token, + result.accessToken.userId, + result.accessToken.expires)); + + if (response.success) { + // User is logged in, test with ParseUser.currentUser() + } + break; + case FacebookLoginStatus.cancelledByUser: + // User cancelled + break; + case FacebookLoginStatus.error: + // Error + break; + } + } +``` + +For Google and the example below, we used the library provided at https://pub.dev/packages/google_sign_in + +``` +class OAuthLogin { + final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'https://www.googleapis.com/auth/contacts.readonly'] ); + + sigInGoogle() async { + GoogleSignInAccount account = await _googleSignIn.signIn(); + GoogleSignInAuthentication authentication = await account.authentication; + await ParseUser.loginWith( + 'google', + google(_googleSignIn.currentUser.id, + authentication.accessToken, + authentication.idToken)); + } +} +``` + +## Security for Objects - ParseACL +For any object, you can specify which users are allowed to read the object, and which users are allowed to modify an object. +To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. + +If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write. +The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. +To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that +limits access to that user. An object’s ACL is updated when the object is saved, like any other property. + +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(owner: user); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using +__setReadAccess__ and __setWriteAccess__ +```dart +ParseUser user = await ParseUser.currentUser() as ParseUser; +ParseACL parseACL = ParseACL(); +//grant total access to current user +parseACL.setReadAccess(userId: user.objectId, allowed: true); +parseACL.setWriteAccess(userId: user.objectId, allowed: true); +//grant read access to userId: 'TjRuDjuSAO' +parseACL.setReadAccess(userId: 'TjRuDjuSAO', allowed: true); +parseACL.setWriteAccess(userId: 'TjRuDjuSAO', allowed: false); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +You can also grant permissions to all users at once using setPublicReadAccess and setPublicWriteAccess. +```dart +ParseACL parseACL = ParseACL(); +parseACL.setPublicReadAccess(allowed: true); +parseACL.setPublicWriteAccess(allowed: true); + +ParseObject parseObject = ParseObject("TestAPI"); +... +parseObject.setACL(parseACL); +var apiResponse = await parseObject.save(); +``` +Operations that are forbidden, such as deleting an object that you do not have write access to, result in a +ParseError with code 101: 'ObjectNotFound'. +For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which +object ids do not exist at all. + +You can retrieve the ACL list of an object using: +```dart +ParseACL parseACL = parseObject.getACL(); +``` + +## Config +The SDK supports Parse Config. A map of all configs can be grabbed from the server by calling : +```dart +var response = await ParseConfig().getConfigs(); +``` + +and to add a config: +```dart +ParseConfig().addConfig('TestConfig', 'testing'); +``` + +## Cloud Functions +The SDK supports call Cloud Functions. + +Executes a cloud function that returns a ParseObject type +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final ParseResponse result = + await function.executeObjectFunction(); +if (result.success) { + if (result.result is ParseObject) { + final ParseObject parseObject = result.result; + print(parseObject.className); + } +} +``` + +Executes a cloud function with parameters +```dart +final ParseCloudFunction function = ParseCloudFunction('hello'); +final Map params = {'plan': 'paid'}; +function.execute(parameters: params); +``` + +## Relation + +The SDK supports Relation. + +To add relation to object: + +```dart +dietPlan.addRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); +``` + +To remove relation to object: + +```dart +dietPlan.removeRelation('fruits', [ParseObject("Fruits")..set("objectId", "XGadzYxnac")]); +``` + +To Retrive a relation instance for user, call: +```dart +final relation = dietPlan.getRelation('fruits'); +``` + +and then you can add a relation to the passed in object: +``` +relation.add(dietPlan); +final result = await user.save(); +``` + +To retrieve objects that are members of Relation field of a parent object: +```dart +QueryBuilder query = + QueryBuilder(ParseObject('Fruits')) + ..whereRelatedTo('fruits', 'DietPlan', DietPlan.objectId); +``` + +## File +There are three different file classes in this SDK: +- `ParseFileBase` is and abstract class and is the foundation of every file class that can be handled by this SDK. +- `ParseFile` (former the only file class in the SDK) extends ParseFileBase and is by default used as the file class on every platform but web. +This class uses a `File` from `dart:io` for storing the raw file. +- `ParseWebFile` is the equivalent to ParseFile used at Flutter Web. +This class uses an `Uint8List` for storing the raw file. + +These classes are used by default to represent files, but you can also build your own class extending ParseFileBase and provide a custom `ParseFileConstructor` similar to the `SubClasses`. + +Have a look at the example application for a small (non web) example. + + +```dart +//A short example for showing an image from a ParseFileBase +Widget buildImage(ParseFileBase image){ + return FutureBuilder( + future: image.download(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (kIsWeb) { + return Image.memory((snapshot.data as ParseWebFile).file); + } else { + return Image.file((snapshot.data as ParseFile).file); + } + } else { + return CircularProgressIndicator(); + } + }, + ); +} +``` +```dart +//A short example for storing a selected picture +//libraries: image_picker (https://pub.dev/packages/image_picker), image_picker_for_web (https://pub.dev/packages/image_picker_for_web) +PickedFile pickedFile = await ImagePicker().getImage(source: ImageSource.gallery); +ParseFileBase parseFile; +if (kIsWeb) { + //Seems weird, but this lets you get the data from the selected file as an Uint8List very easily. + ParseWebFile file = ParseWebFile(null, name: null, url: pickedFile.path); + await file.download(); + parseFile = ParseWebFile(file.file, name: file.name); +} else { + parseFile = ParseFile(File(pickedFile.path)); +} +someParseObject.set("image", parseFile); +//This saves the ParseObject as well as all of its children, and the ParseFileBase is such a child. +await someParseObject.save(); +``` + +## Other Features of this library +Main: +* Installation (View the example application) +* GeoPoints (View the example application) +* Persistent storage +* Debug Mode - Logging API calls +* Manage Session ID's tokens + +User: +* Queries +* Anonymous (View the example application) +* 3rd Party Authentication + +Objects: +* Create new object +* Extend Parse Object and create local objects that can be saved and retreived +* Queries + +## Author:- +This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com diff --git a/example/.gitignore b/packages/flutter/example/.gitignore similarity index 100% rename from example/.gitignore rename to packages/flutter/example/.gitignore diff --git a/example/.metadata b/packages/flutter/example/.metadata similarity index 100% rename from example/.metadata rename to packages/flutter/example/.metadata diff --git a/example/README.md b/packages/flutter/example/README.md similarity index 100% rename from example/README.md rename to packages/flutter/example/README.md diff --git a/example/android/.gitignore b/packages/flutter/example/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to packages/flutter/example/android/.gitignore diff --git a/example/android/.project b/packages/flutter/example/android/.project similarity index 100% rename from example/android/.project rename to packages/flutter/example/android/.project diff --git a/example/android/.settings/org.eclipse.buildship.core.prefs b/packages/flutter/example/android/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from example/android/.settings/org.eclipse.buildship.core.prefs rename to packages/flutter/example/android/.settings/org.eclipse.buildship.core.prefs diff --git a/example/android/app/.classpath b/packages/flutter/example/android/app/.classpath similarity index 100% rename from example/android/app/.classpath rename to packages/flutter/example/android/app/.classpath diff --git a/example/android/app/.project b/packages/flutter/example/android/app/.project similarity index 100% rename from example/android/app/.project rename to packages/flutter/example/android/app/.project diff --git a/example/android/app/.settings/org.eclipse.buildship.core.prefs b/packages/flutter/example/android/app/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from example/android/app/.settings/org.eclipse.buildship.core.prefs rename to packages/flutter/example/android/app/.settings/org.eclipse.buildship.core.prefs diff --git a/example/android/app/build.gradle b/packages/flutter/example/android/app/build.gradle similarity index 100% rename from example/android/app/build.gradle rename to packages/flutter/example/android/app/build.gradle diff --git a/example/android/app/src/main/AndroidManifest.xml b/packages/flutter/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to packages/flutter/example/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/gen/com/example/flutterpluginexample/BuildConfig.java b/packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/BuildConfig.java similarity index 100% rename from example/android/app/src/main/gen/com/example/flutterpluginexample/BuildConfig.java rename to packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/BuildConfig.java diff --git a/example/android/app/src/main/gen/com/example/flutterpluginexample/Manifest.java b/packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/Manifest.java similarity index 100% rename from example/android/app/src/main/gen/com/example/flutterpluginexample/Manifest.java rename to packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/Manifest.java diff --git a/example/android/app/src/main/gen/com/example/flutterpluginexample/R.java b/packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/R.java similarity index 100% rename from example/android/app/src/main/gen/com/example/flutterpluginexample/R.java rename to packages/flutter/example/android/app/src/main/gen/com/example/flutterpluginexample/R.java diff --git a/example/android/app/src/main/java/com/example/flutterpluginexample/MainActivity.java b/packages/flutter/example/android/app/src/main/java/com/example/flutterpluginexample/MainActivity.java similarity index 100% rename from example/android/app/src/main/java/com/example/flutterpluginexample/MainActivity.java rename to packages/flutter/example/android/app/src/main/java/com/example/flutterpluginexample/MainActivity.java diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/packages/flutter/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to packages/flutter/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values/styles.xml b/packages/flutter/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to packages/flutter/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/build.gradle b/packages/flutter/example/android/build.gradle similarity index 100% rename from example/android/build.gradle rename to packages/flutter/example/android/build.gradle diff --git a/example/android/gradle.properties b/packages/flutter/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to packages/flutter/example/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/flutter/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/settings.gradle b/packages/flutter/example/android/settings.gradle similarity index 100% rename from example/android/settings.gradle rename to packages/flutter/example/android/settings.gradle diff --git a/example/assets/parse.png b/packages/flutter/example/assets/parse.png similarity index 100% rename from example/assets/parse.png rename to packages/flutter/example/assets/parse.png diff --git a/example/fonts/Roboto/LICENSE.txt b/packages/flutter/example/fonts/Roboto/LICENSE.txt similarity index 100% rename from example/fonts/Roboto/LICENSE.txt rename to packages/flutter/example/fonts/Roboto/LICENSE.txt diff --git a/example/fonts/Roboto/Roboto-Black.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Black.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Black.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Black.ttf diff --git a/example/fonts/Roboto/Roboto-Bold.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Bold.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Bold.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Bold.ttf diff --git a/example/fonts/Roboto/Roboto-Light.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Light.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Light.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Light.ttf diff --git a/example/fonts/Roboto/Roboto-Medium.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Medium.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Medium.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Medium.ttf diff --git a/example/fonts/Roboto/Roboto-Regular.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Regular.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Regular.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Regular.ttf diff --git a/example/fonts/Roboto/Roboto-Thin.ttf b/packages/flutter/example/fonts/Roboto/Roboto-Thin.ttf similarity index 100% rename from example/fonts/Roboto/Roboto-Thin.ttf rename to packages/flutter/example/fonts/Roboto/Roboto-Thin.ttf diff --git a/example/ios/.gitignore b/packages/flutter/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to packages/flutter/example/ios/.gitignore diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to packages/flutter/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/example/ios/Flutter/Debug.xcconfig b/packages/flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to packages/flutter/example/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/packages/flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to packages/flutter/example/ios/Flutter/Release.xcconfig diff --git a/example/ios/Podfile b/packages/flutter/example/ios/Podfile similarity index 100% rename from example/ios/Podfile rename to packages/flutter/example/ios/Podfile diff --git a/example/ios/Podfile.lock b/packages/flutter/example/ios/Podfile.lock similarity index 100% rename from example/ios/Podfile.lock rename to packages/flutter/example/ios/Podfile.lock diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to packages/flutter/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.h b/packages/flutter/example/ios/Runner/AppDelegate.h similarity index 100% rename from example/ios/Runner/AppDelegate.h rename to packages/flutter/example/ios/Runner/AppDelegate.h diff --git a/example/ios/Runner/AppDelegate.m b/packages/flutter/example/ios/Runner/AppDelegate.m similarity index 100% rename from example/ios/Runner/AppDelegate.m rename to packages/flutter/example/ios/Runner/AppDelegate.m diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to packages/flutter/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/example/ios/Runner/Info.plist b/packages/flutter/example/ios/Runner/Info.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to packages/flutter/example/ios/Runner/Info.plist diff --git a/example/ios/Runner/main.m b/packages/flutter/example/ios/Runner/main.m similarity index 100% rename from example/ios/Runner/main.m rename to packages/flutter/example/ios/Runner/main.m diff --git a/example/lib/data/base/api_error.dart b/packages/flutter/example/lib/data/base/api_error.dart similarity index 100% rename from example/lib/data/base/api_error.dart rename to packages/flutter/example/lib/data/base/api_error.dart diff --git a/example/lib/data/base/api_response.dart b/packages/flutter/example/lib/data/base/api_response.dart similarity index 100% rename from example/lib/data/base/api_response.dart rename to packages/flutter/example/lib/data/base/api_response.dart diff --git a/example/lib/data/model/day.dart b/packages/flutter/example/lib/data/model/day.dart similarity index 100% rename from example/lib/data/model/day.dart rename to packages/flutter/example/lib/data/model/day.dart diff --git a/example/lib/data/model/diet_plan.dart b/packages/flutter/example/lib/data/model/diet_plan.dart similarity index 100% rename from example/lib/data/model/diet_plan.dart rename to packages/flutter/example/lib/data/model/diet_plan.dart diff --git a/example/lib/data/model/user.dart b/packages/flutter/example/lib/data/model/user.dart similarity index 100% rename from example/lib/data/model/user.dart rename to packages/flutter/example/lib/data/model/user.dart diff --git a/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart b/packages/flutter/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart similarity index 100% rename from example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart rename to packages/flutter/example/lib/data/repositories/diet_plan/contract_provider_diet_plan.dart diff --git a/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart b/packages/flutter/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart similarity index 100% rename from example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart rename to packages/flutter/example/lib/data/repositories/diet_plan/provider_api_diet_plan.dart diff --git a/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart b/packages/flutter/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart similarity index 100% rename from example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart rename to packages/flutter/example/lib/data/repositories/diet_plan/provider_db_diet_plan.dart diff --git a/example/lib/data/repositories/diet_plan/repository_diet_plan.dart b/packages/flutter/example/lib/data/repositories/diet_plan/repository_diet_plan.dart similarity index 100% rename from example/lib/data/repositories/diet_plan/repository_diet_plan.dart rename to packages/flutter/example/lib/data/repositories/diet_plan/repository_diet_plan.dart diff --git a/example/lib/data/repositories/user/contract_provider_user.dart b/packages/flutter/example/lib/data/repositories/user/contract_provider_user.dart similarity index 100% rename from example/lib/data/repositories/user/contract_provider_user.dart rename to packages/flutter/example/lib/data/repositories/user/contract_provider_user.dart diff --git a/example/lib/data/repositories/user/provider_api_user.dart b/packages/flutter/example/lib/data/repositories/user/provider_api_user.dart similarity index 100% rename from example/lib/data/repositories/user/provider_api_user.dart rename to packages/flutter/example/lib/data/repositories/user/provider_api_user.dart diff --git a/example/lib/data/repositories/user/provider_db_user.dart b/packages/flutter/example/lib/data/repositories/user/provider_db_user.dart similarity index 100% rename from example/lib/data/repositories/user/provider_db_user.dart rename to packages/flutter/example/lib/data/repositories/user/provider_db_user.dart diff --git a/example/lib/data/repositories/user/repository_user.dart b/packages/flutter/example/lib/data/repositories/user/repository_user.dart similarity index 100% rename from example/lib/data/repositories/user/repository_user.dart rename to packages/flutter/example/lib/data/repositories/user/repository_user.dart diff --git a/example/lib/domain/constants/application_constants.dart b/packages/flutter/example/lib/domain/constants/application_constants.dart similarity index 100% rename from example/lib/domain/constants/application_constants.dart rename to packages/flutter/example/lib/domain/constants/application_constants.dart diff --git a/example/lib/domain/utils/collection_utils.dart b/packages/flutter/example/lib/domain/utils/collection_utils.dart similarity index 100% rename from example/lib/domain/utils/collection_utils.dart rename to packages/flutter/example/lib/domain/utils/collection_utils.dart diff --git a/example/lib/domain/utils/db_utils.dart b/packages/flutter/example/lib/domain/utils/db_utils.dart similarity index 100% rename from example/lib/domain/utils/db_utils.dart rename to packages/flutter/example/lib/domain/utils/db_utils.dart diff --git a/example/lib/main.dart b/packages/flutter/example/lib/main.dart similarity index 99% rename from example/lib/main.dart rename to packages/flutter/example/lib/main.dart index aee8d831e..22c7bdae6 100644 --- a/example/lib/main.dart +++ b/packages/flutter/example/lib/main.dart @@ -11,7 +11,7 @@ import 'package:flutter_plugin_example/data/repositories/user/repository_user.da import 'package:flutter_plugin_example/domain/constants/application_constants.dart'; import 'package:flutter_plugin_example/domain/utils/db_utils.dart'; import 'package:flutter_plugin_example/pages/decision_page.dart'; -import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; void main() { _setTargetPlatformForDesktop(); diff --git a/example/lib/pages/decision_page.dart b/packages/flutter/example/lib/pages/decision_page.dart similarity index 100% rename from example/lib/pages/decision_page.dart rename to packages/flutter/example/lib/pages/decision_page.dart diff --git a/example/lib/pages/home_page.dart b/packages/flutter/example/lib/pages/home_page.dart similarity index 100% rename from example/lib/pages/home_page.dart rename to packages/flutter/example/lib/pages/home_page.dart diff --git a/example/lib/pages/login_page.dart b/packages/flutter/example/lib/pages/login_page.dart similarity index 100% rename from example/lib/pages/login_page.dart rename to packages/flutter/example/lib/pages/login_page.dart diff --git a/example/linux/.gitignore b/packages/flutter/example/linux/.gitignore similarity index 100% rename from example/linux/.gitignore rename to packages/flutter/example/linux/.gitignore diff --git a/example/linux/Makefile b/packages/flutter/example/linux/Makefile similarity index 100% rename from example/linux/Makefile rename to packages/flutter/example/linux/Makefile diff --git a/example/linux/flutter_embedder_example.cc b/packages/flutter/example/linux/flutter_embedder_example.cc similarity index 100% rename from example/linux/flutter_embedder_example.cc rename to packages/flutter/example/linux/flutter_embedder_example.cc diff --git a/example/macos/.gitignore b/packages/flutter/example/macos/.gitignore similarity index 100% rename from example/macos/.gitignore rename to packages/flutter/example/macos/.gitignore diff --git a/example/macos/AppDelegate.swift b/packages/flutter/example/macos/AppDelegate.swift similarity index 100% rename from example/macos/AppDelegate.swift rename to packages/flutter/example/macos/AppDelegate.swift diff --git a/example/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter/example/macos/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/macos/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/flutter/example/macos/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/macos/Base.lproj/MainMenu.xib b/packages/flutter/example/macos/Base.lproj/MainMenu.xib similarity index 100% rename from example/macos/Base.lproj/MainMenu.xib rename to packages/flutter/example/macos/Base.lproj/MainMenu.xib diff --git a/example/macos/ExampleWindow.swift b/packages/flutter/example/macos/ExampleWindow.swift similarity index 100% rename from example/macos/ExampleWindow.swift rename to packages/flutter/example/macos/ExampleWindow.swift diff --git a/example/macos/Flutter/Debug.xcconfig b/packages/flutter/example/macos/Flutter/Debug.xcconfig similarity index 100% rename from example/macos/Flutter/Debug.xcconfig rename to packages/flutter/example/macos/Flutter/Debug.xcconfig diff --git a/example/macos/Flutter/Release.xcconfig b/packages/flutter/example/macos/Flutter/Release.xcconfig similarity index 100% rename from example/macos/Flutter/Release.xcconfig rename to packages/flutter/example/macos/Flutter/Release.xcconfig diff --git a/example/macos/Info.plist b/packages/flutter/example/macos/Info.plist similarity index 100% rename from example/macos/Info.plist rename to packages/flutter/example/macos/Info.plist diff --git a/example/macos/PluginRegistrant.h b/packages/flutter/example/macos/PluginRegistrant.h similarity index 100% rename from example/macos/PluginRegistrant.h rename to packages/flutter/example/macos/PluginRegistrant.h diff --git a/example/macos/PluginRegistrant.m b/packages/flutter/example/macos/PluginRegistrant.m similarity index 100% rename from example/macos/PluginRegistrant.m rename to packages/flutter/example/macos/PluginRegistrant.m diff --git a/packages/flutter/example/macos/Podfile b/packages/flutter/example/macos/Podfile new file mode 100644 index 000000000..d60ec7102 --- /dev/null +++ b/packages/flutter/example/macos/Podfile @@ -0,0 +1,82 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + pods_ary = [] + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) { |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + pods_ary.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_ary +end + +def pubspec_supports_macos(file) + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return false; + end + File.foreach(file_abs_path) { |line| + return true if line =~ /^\s*macos:/ + } + return false +end + +target 'Runner' do + use_frameworks! + use_modular_headers! + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + ephemeral_dir = File.join('Flutter', 'ephemeral') + symlink_dir = File.join(ephemeral_dir, '.symlinks') + symlink_plugins_dir = File.join(symlink_dir, 'plugins') + system("rm -rf #{symlink_dir}") + system("mkdir -p #{symlink_plugins_dir}") + + # Flutter Pods + generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig')) + if generated_xcconfig.empty? + puts "Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." + end + generated_xcconfig.map { |p| + if p[:name] == 'FLUTTER_FRAMEWORK_DIR' + symlink = File.join(symlink_dir, 'flutter') + File.symlink(File.dirname(p[:path]), symlink) + pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path])) + end + } + + # Plugin Pods + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.map { |p| + symlink = File.join(symlink_plugins_dir, p[:name]) + File.symlink(p[:path], symlink) + if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml')) + pod p[:name], :path => File.join(symlink, 'macos') + end + } +end + +# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. +install! 'cocoapods', :disable_input_output_paths => true diff --git a/example/macos/Runner-Bridging-Header.h b/packages/flutter/example/macos/Runner-Bridging-Header.h similarity index 100% rename from example/macos/Runner-Bridging-Header.h rename to packages/flutter/example/macos/Runner-Bridging-Header.h diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/packages/flutter/example/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/macos/Runner.xcodeproj/project.pbxproj rename to packages/flutter/example/macos/Runner.xcodeproj/project.pbxproj diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/flutter/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/flutter/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/macos/name_output.sh b/packages/flutter/example/macos/name_output.sh similarity index 100% rename from example/macos/name_output.sh rename to packages/flutter/example/macos/name_output.sh diff --git a/example/pubspec.yaml b/packages/flutter/example/pubspec.yaml similarity index 98% rename from example/pubspec.yaml rename to packages/flutter/example/pubspec.yaml index 889345e65..666274921 100644 --- a/example/pubspec.yaml +++ b/packages/flutter/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: path_provider: ^1.6.14 dev_dependencies: - parse_server_sdk: + parse_server_sdk_flutter: path: ../ flutter_test: sdk: flutter diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart b/packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart similarity index 100% rename from example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart rename to packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_api_test.dart diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart b/packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart similarity index 100% rename from example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart rename to packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_db_test.dart diff --git a/example/test/data/repository/diet_plan/repository_diet_plan_test.dart b/packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_test.dart similarity index 100% rename from example/test/data/repository/diet_plan/repository_diet_plan_test.dart rename to packages/flutter/example/test/data/repository/diet_plan/repository_diet_plan_test.dart diff --git a/example/test/data/repository/repository_mock_utils.dart b/packages/flutter/example/test/data/repository/repository_mock_utils.dart similarity index 100% rename from example/test/data/repository/repository_mock_utils.dart rename to packages/flutter/example/test/data/repository/repository_mock_utils.dart diff --git a/example/windows/.gitignore b/packages/flutter/example/windows/.gitignore similarity index 100% rename from example/windows/.gitignore rename to packages/flutter/example/windows/.gitignore diff --git a/example/windows/Runner.sln b/packages/flutter/example/windows/Runner.sln similarity index 100% rename from example/windows/Runner.sln rename to packages/flutter/example/windows/Runner.sln diff --git a/example/windows/Runner.vcxproj b/packages/flutter/example/windows/Runner.vcxproj similarity index 100% rename from example/windows/Runner.vcxproj rename to packages/flutter/example/windows/Runner.vcxproj diff --git a/example/windows/Runner.vcxproj.filters b/packages/flutter/example/windows/Runner.vcxproj.filters similarity index 100% rename from example/windows/Runner.vcxproj.filters rename to packages/flutter/example/windows/Runner.vcxproj.filters diff --git a/example/windows/build.bat b/packages/flutter/example/windows/build.bat similarity index 100% rename from example/windows/build.bat rename to packages/flutter/example/windows/build.bat diff --git a/example/windows/find_vcvars.dart b/packages/flutter/example/windows/find_vcvars.dart similarity index 100% rename from example/windows/find_vcvars.dart rename to packages/flutter/example/windows/find_vcvars.dart diff --git a/example/windows/flutter_embedder_example.cpp b/packages/flutter/example/windows/flutter_embedder_example.cpp similarity index 100% rename from example/windows/flutter_embedder_example.cpp rename to packages/flutter/example/windows/flutter_embedder_example.cpp diff --git a/example/windows/generate_props.dart b/packages/flutter/example/windows/generate_props.dart similarity index 100% rename from example/windows/generate_props.dart rename to packages/flutter/example/windows/generate_props.dart diff --git a/example/windows/name_output.bat b/packages/flutter/example/windows/name_output.bat similarity index 100% rename from example/windows/name_output.bat rename to packages/flutter/example/windows/name_output.bat diff --git a/example/windows/scripts/bundle_assets_and_deps.bat b/packages/flutter/example/windows/scripts/bundle_assets_and_deps.bat similarity index 100% rename from example/windows/scripts/bundle_assets_and_deps.bat rename to packages/flutter/example/windows/scripts/bundle_assets_and_deps.bat diff --git a/example/windows/scripts/prepare_dependencies.bat b/packages/flutter/example/windows/scripts/prepare_dependencies.bat similarity index 100% rename from example/windows/scripts/prepare_dependencies.bat rename to packages/flutter/example/windows/scripts/prepare_dependencies.bat diff --git a/example_livelist/.gitignore b/packages/flutter/example_livelist/.gitignore similarity index 100% rename from example_livelist/.gitignore rename to packages/flutter/example_livelist/.gitignore diff --git a/example_livelist/.metadata b/packages/flutter/example_livelist/.metadata similarity index 100% rename from example_livelist/.metadata rename to packages/flutter/example_livelist/.metadata diff --git a/example_livelist/README.md b/packages/flutter/example_livelist/README.md similarity index 100% rename from example_livelist/README.md rename to packages/flutter/example_livelist/README.md diff --git a/example_livelist/android/.gitignore b/packages/flutter/example_livelist/android/.gitignore similarity index 100% rename from example_livelist/android/.gitignore rename to packages/flutter/example_livelist/android/.gitignore diff --git a/example_livelist/android/app/build.gradle b/packages/flutter/example_livelist/android/app/build.gradle similarity index 100% rename from example_livelist/android/app/build.gradle rename to packages/flutter/example_livelist/android/app/build.gradle diff --git a/example_livelist/android/app/src/debug/AndroidManifest.xml b/packages/flutter/example_livelist/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example_livelist/android/app/src/debug/AndroidManifest.xml rename to packages/flutter/example_livelist/android/app/src/debug/AndroidManifest.xml diff --git a/example_livelist/android/app/src/main/AndroidManifest.xml b/packages/flutter/example_livelist/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example_livelist/android/app/src/main/AndroidManifest.xml rename to packages/flutter/example_livelist/android/app/src/main/AndroidManifest.xml diff --git a/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt b/packages/flutter/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt similarity index 100% rename from example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt rename to packages/flutter/example_livelist/android/app/src/main/kotlin/com/example/example_livelist/MainActivity.kt diff --git a/example_livelist/android/app/src/main/kotlin/com/example/flutterpluginexample_livelist/MainActivity.kt b/packages/flutter/example_livelist/android/app/src/main/kotlin/com/example/flutterpluginexample_livelist/MainActivity.kt similarity index 100% rename from example_livelist/android/app/src/main/kotlin/com/example/flutterpluginexample_livelist/MainActivity.kt rename to packages/flutter/example_livelist/android/app/src/main/kotlin/com/example/flutterpluginexample_livelist/MainActivity.kt diff --git a/example_livelist/android/app/src/main/res/drawable/launch_background.xml b/packages/flutter/example_livelist/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example_livelist/android/app/src/main/res/drawable/launch_background.xml rename to packages/flutter/example_livelist/android/app/src/main/res/drawable/launch_background.xml diff --git a/example_livelist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/flutter/example_livelist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example_livelist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/flutter/example_livelist/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example_livelist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/flutter/example_livelist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example_livelist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/flutter/example_livelist/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example_livelist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/flutter/example_livelist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example_livelist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/flutter/example_livelist/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example_livelist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/flutter/example_livelist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example_livelist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/flutter/example_livelist/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example_livelist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/flutter/example_livelist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example_livelist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/flutter/example_livelist/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example_livelist/android/app/src/main/res/values/styles.xml b/packages/flutter/example_livelist/android/app/src/main/res/values/styles.xml similarity index 100% rename from example_livelist/android/app/src/main/res/values/styles.xml rename to packages/flutter/example_livelist/android/app/src/main/res/values/styles.xml diff --git a/example_livelist/android/app/src/profile/AndroidManifest.xml b/packages/flutter/example_livelist/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example_livelist/android/app/src/profile/AndroidManifest.xml rename to packages/flutter/example_livelist/android/app/src/profile/AndroidManifest.xml diff --git a/example_livelist/android/build.gradle b/packages/flutter/example_livelist/android/build.gradle similarity index 100% rename from example_livelist/android/build.gradle rename to packages/flutter/example_livelist/android/build.gradle diff --git a/example_livelist/android/gradle.properties b/packages/flutter/example_livelist/android/gradle.properties similarity index 100% rename from example_livelist/android/gradle.properties rename to packages/flutter/example_livelist/android/gradle.properties diff --git a/example_livelist/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter/example_livelist/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example_livelist/android/gradle/wrapper/gradle-wrapper.properties rename to packages/flutter/example_livelist/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example_livelist/android/settings.gradle b/packages/flutter/example_livelist/android/settings.gradle similarity index 100% rename from example_livelist/android/settings.gradle rename to packages/flutter/example_livelist/android/settings.gradle diff --git a/example_livelist/assets/parse.png b/packages/flutter/example_livelist/assets/parse.png similarity index 100% rename from example_livelist/assets/parse.png rename to packages/flutter/example_livelist/assets/parse.png diff --git a/example_livelist/fonts/Roboto/LICENSE.txt b/packages/flutter/example_livelist/fonts/Roboto/LICENSE.txt similarity index 100% rename from example_livelist/fonts/Roboto/LICENSE.txt rename to packages/flutter/example_livelist/fonts/Roboto/LICENSE.txt diff --git a/example_livelist/fonts/Roboto/Roboto-Black.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Black.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Black.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Black.ttf diff --git a/example_livelist/fonts/Roboto/Roboto-Bold.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Bold.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Bold.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Bold.ttf diff --git a/example_livelist/fonts/Roboto/Roboto-Light.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Light.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Light.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Light.ttf diff --git a/example_livelist/fonts/Roboto/Roboto-Medium.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Medium.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Medium.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Medium.ttf diff --git a/example_livelist/fonts/Roboto/Roboto-Regular.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Regular.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Regular.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Regular.ttf diff --git a/example_livelist/fonts/Roboto/Roboto-Thin.ttf b/packages/flutter/example_livelist/fonts/Roboto/Roboto-Thin.ttf similarity index 100% rename from example_livelist/fonts/Roboto/Roboto-Thin.ttf rename to packages/flutter/example_livelist/fonts/Roboto/Roboto-Thin.ttf diff --git a/example_livelist/ios/.gitignore b/packages/flutter/example_livelist/ios/.gitignore similarity index 100% rename from example_livelist/ios/.gitignore rename to packages/flutter/example_livelist/ios/.gitignore diff --git a/example_livelist/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter/example_livelist/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example_livelist/ios/Flutter/AppFrameworkInfo.plist rename to packages/flutter/example_livelist/ios/Flutter/AppFrameworkInfo.plist diff --git a/example_livelist/ios/Flutter/Debug.xcconfig b/packages/flutter/example_livelist/ios/Flutter/Debug.xcconfig similarity index 100% rename from example_livelist/ios/Flutter/Debug.xcconfig rename to packages/flutter/example_livelist/ios/Flutter/Debug.xcconfig diff --git a/example_livelist/ios/Flutter/Release.xcconfig b/packages/flutter/example_livelist/ios/Flutter/Release.xcconfig similarity index 100% rename from example_livelist/ios/Flutter/Release.xcconfig rename to packages/flutter/example_livelist/ios/Flutter/Release.xcconfig diff --git a/example_livelist/ios/Podfile b/packages/flutter/example_livelist/ios/Podfile similarity index 100% rename from example_livelist/ios/Podfile rename to packages/flutter/example_livelist/ios/Podfile diff --git a/example_livelist/ios/Podfile.lock b/packages/flutter/example_livelist/ios/Podfile.lock similarity index 100% rename from example_livelist/ios/Podfile.lock rename to packages/flutter/example_livelist/ios/Podfile.lock diff --git a/example_livelist/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter/example_livelist/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example_livelist/ios/Runner.xcodeproj/project.pbxproj rename to packages/flutter/example_livelist/ios/Runner.xcodeproj/project.pbxproj diff --git a/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example_livelist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/flutter/example_livelist/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example_livelist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter/example_livelist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example_livelist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/flutter/example_livelist/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/flutter/example_livelist/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/flutter/example_livelist/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/flutter/example_livelist/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example_livelist/ios/Runner/AppDelegate.swift b/packages/flutter/example_livelist/ios/Runner/AppDelegate.swift similarity index 100% rename from example_livelist/ios/Runner/AppDelegate.swift rename to packages/flutter/example_livelist/ios/Runner/AppDelegate.swift diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/flutter/example_livelist/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example_livelist/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/flutter/example_livelist/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example_livelist/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/flutter/example_livelist/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example_livelist/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter/example_livelist/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example_livelist/ios/Runner/Base.lproj/Main.storyboard rename to packages/flutter/example_livelist/ios/Runner/Base.lproj/Main.storyboard diff --git a/example_livelist/ios/Runner/Info.plist b/packages/flutter/example_livelist/ios/Runner/Info.plist similarity index 100% rename from example_livelist/ios/Runner/Info.plist rename to packages/flutter/example_livelist/ios/Runner/Info.plist diff --git a/example_livelist/ios/Runner/Runner-Bridging-Header.h b/packages/flutter/example_livelist/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example_livelist/ios/Runner/Runner-Bridging-Header.h rename to packages/flutter/example_livelist/ios/Runner/Runner-Bridging-Header.h diff --git a/example_livelist/lib/application_constants.dart b/packages/flutter/example_livelist/lib/application_constants.dart similarity index 100% rename from example_livelist/lib/application_constants.dart rename to packages/flutter/example_livelist/lib/application_constants.dart diff --git a/example_livelist/lib/main.dart b/packages/flutter/example_livelist/lib/main.dart similarity index 98% rename from example_livelist/lib/main.dart rename to packages/flutter/example_livelist/lib/main.dart index 14b569689..d27d57330 100644 --- a/example_livelist/lib/main.dart +++ b/packages/flutter/example_livelist/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; import 'application_constants.dart'; diff --git a/example_livelist/pubspec.yaml b/packages/flutter/example_livelist/pubspec.yaml similarity index 98% rename from example_livelist/pubspec.yaml rename to packages/flutter/example_livelist/pubspec.yaml index 05bc5d4d5..339842141 100644 --- a/example_livelist/pubspec.yaml +++ b/packages/flutter/example_livelist/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.0+1 dependencies: flutter: sdk: flutter - parse_server_sdk: + parse_server_sdk_flutter: path: ../ cupertino_icons: ^0.1.2 diff --git a/example_livelist/web/favicon.png b/packages/flutter/example_livelist/web/favicon.png similarity index 100% rename from example_livelist/web/favicon.png rename to packages/flutter/example_livelist/web/favicon.png diff --git a/example_livelist/web/icons/Icon-192.png b/packages/flutter/example_livelist/web/icons/Icon-192.png similarity index 100% rename from example_livelist/web/icons/Icon-192.png rename to packages/flutter/example_livelist/web/icons/Icon-192.png diff --git a/example_livelist/web/icons/Icon-512.png b/packages/flutter/example_livelist/web/icons/Icon-512.png similarity index 100% rename from example_livelist/web/icons/Icon-512.png rename to packages/flutter/example_livelist/web/icons/Icon-512.png diff --git a/example_livelist/web/index.html b/packages/flutter/example_livelist/web/index.html similarity index 100% rename from example_livelist/web/index.html rename to packages/flutter/example_livelist/web/index.html diff --git a/example_livelist/web/manifest.json b/packages/flutter/example_livelist/web/manifest.json similarity index 100% rename from example_livelist/web/manifest.json rename to packages/flutter/example_livelist/web/manifest.json diff --git a/lib/generated/i18n.dart b/packages/flutter/lib/generated/i18n.dart similarity index 100% rename from lib/generated/i18n.dart rename to packages/flutter/lib/generated/i18n.dart diff --git a/lib/parse_server_sdk.dart b/packages/flutter/lib/parse_server_sdk.dart similarity index 91% rename from lib/parse_server_sdk.dart rename to packages/flutter/lib/parse_server_sdk.dart index fa4fd2b21..c8d4d6088 100644 --- a/lib/parse_server_sdk.dart +++ b/packages/flutter/lib/parse_server_sdk.dart @@ -1,20 +1,24 @@ +library flutter_parse_sdk_flutter; + import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:connectivity/connectivity.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:package_info/package_info.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart' as sdk; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:sembast/sembast.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import 'parse_server_sdk_dart.dart' as sdk; -import 'src/storage/core_store_sp_impl.dart'; +export 'package:parse_server_sdk/parse_server_sdk.dart' + hide Parse, CoreStoreSembastImp; -export 'parse_server_sdk_dart.dart' hide Parse, CoreStoreSembastImp; -export 'src/storage/core_store_sp_impl.dart'; -export 'src/utils/parse_live_list_flutter.dart'; +part 'src/storage/core_store_sp_impl.dart'; +part 'src/utils/parse_live_list.dart'; class Parse extends sdk.Parse with WidgetsBindingObserver @@ -57,7 +61,8 @@ class Parse extends sdk.Parse String fileDirectory, Stream appResumedStream, }) async { - if (!sdk.parseIsWeb && (appName == null || appVersion == null || appPackageName == null)) { + if (!sdk.parseIsWeb && + (appName == null || appVersion == null || appPackageName == null)) { final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appName ??= packageInfo.appName; appVersion ??= packageInfo.version; @@ -135,7 +140,8 @@ class CoreStoreSembastImp implements sdk.CoreStoreSembastImp { static sdk.CoreStoreSembastImp _sembastImp; - static Future getInstance({DatabaseFactory factory, String password}) async { + static Future getInstance( + {DatabaseFactory factory, String password}) async { if (_sembastImp == null) { String dbDirectory = ''; if (!sdk.parseIsWeb && diff --git a/lib/src/storage/core_store_sp_impl.dart b/packages/flutter/lib/src/storage/core_store_sp_impl.dart similarity index 90% rename from lib/src/storage/core_store_sp_impl.dart rename to packages/flutter/lib/src/storage/core_store_sp_impl.dart index e41693c43..63f54de44 100644 --- a/lib/src/storage/core_store_sp_impl.dart +++ b/packages/flutter/lib/src/storage/core_store_sp_impl.dart @@ -1,12 +1,11 @@ -import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +part of flutter_parse_sdk_flutter; -class CoreStoreSharedPrefsImp implements CoreStore { +class CoreStoreSharedPrefsImp implements sdk.CoreStore { CoreStoreSharedPrefsImp._internal(this._store); static CoreStoreSharedPrefsImp _instance; - static Future getInstance( + static Future getInstance( {SharedPreferences store, String password = 'flutter_sdk'}) async { if (_instance == null) { store ??= await SharedPreferences.getInstance(); diff --git a/lib/src/utils/parse_live_list_flutter.dart b/packages/flutter/lib/src/utils/parse_live_list.dart similarity index 76% rename from lib/src/utils/parse_live_list_flutter.dart rename to packages/flutter/lib/src/utils/parse_live_list.dart index 1d6eb394a..a4674dac5 100644 --- a/lib/src/utils/parse_live_list_flutter.dart +++ b/packages/flutter/lib/src/utils/parse_live_list.dart @@ -1,12 +1,9 @@ -import 'dart:async'; +part of flutter_parse_sdk_flutter; -import 'package:flutter/material.dart'; -import 'package:parse_server_sdk/parse_server_sdk_dart.dart'; +typedef ChildBuilder = Widget Function( + BuildContext context, sdk.ParseLiveListElementSnapshot snapshot); -typedef ChildBuilder = Widget Function( - BuildContext context, ParseLiveListElementSnapshot snapshot); - -class ParseLiveListWidget extends StatefulWidget { +class ParseLiveListWidget extends StatefulWidget { const ParseLiveListWidget({ Key key, @required this.query, @@ -26,7 +23,7 @@ class ParseLiveListWidget extends StatefulWidget { this.lazyLoading = true, }) : super(key: key); - final QueryBuilder query; + final sdk.QueryBuilder query; final Widget listLoadingElement; final Duration duration; final ScrollPhysics scrollPhysics; @@ -55,15 +52,15 @@ class ParseLiveListWidget extends StatefulWidget { lazyLoading: lazyLoading, ); - static Widget defaultChildBuilder( - BuildContext context, ParseLiveListElementSnapshot snapshot) { + static Widget defaultChildBuilder( + BuildContext context, sdk.ParseLiveListElementSnapshot snapshot) { Widget child; if (snapshot.failed) { child = const Text('something went wrong!'); } else if (snapshot.hasData) { child = ListTile( title: Text( - snapshot.loadedData.get(keyVarObjectId), + snapshot.loadedData.get(sdk.keyVarObjectId), ), ); } else { @@ -75,7 +72,7 @@ class ParseLiveListWidget extends StatefulWidget { } } -class _ParseLiveListWidgetState +class _ParseLiveListWidgetState extends State> { _ParseLiveListWidgetState( {@required this.query, @@ -83,26 +80,27 @@ class _ParseLiveListWidgetState bool listenOnAllSubItems, List listeningIncludes, bool lazyLoading = true}) { - ParseLiveList.create( + sdk.ParseLiveList.create( query, listenOnAllSubItems: listenOnAllSubItems, listeningIncludes: listeningIncludes, lazyLoading: lazyLoading, - ).then((ParseLiveList value) { + ).then((sdk.ParseLiveList value) { setState(() { _liveList = value; - _liveList.stream.listen((ParseLiveListEvent event) { - if (event is ParseLiveListAddEvent) { + _liveList.stream + .listen((sdk.ParseLiveListEvent event) { + if (event is sdk.ParseLiveListAddEvent) { if (_animatedListKey.currentState != null) _animatedListKey.currentState .insertItem(event.index, duration: widget.duration); - } else if (event is ParseLiveListDeleteEvent) { + } else if (event is sdk.ParseLiveListDeleteEvent) { _animatedListKey.currentState.removeItem( event.index, (BuildContext context, Animation animation) => ParseLiveListElementWidget( key: ValueKey(event.object?.get( - keyVarObjectId, + sdk.keyVarObjectId, defaultValue: 'removingItem')), childBuilder: widget.childBuilder ?? ParseLiveListWidget.defaultChildBuilder, @@ -117,8 +115,8 @@ class _ParseLiveListWidgetState }); } - final QueryBuilder query; - ParseLiveList _liveList; + final sdk.QueryBuilder query; + sdk.ParseLiveList _liveList; final GlobalKey _animatedListKey = GlobalKey(); final ChildBuilder removedItemBuilder; @@ -164,7 +162,8 @@ class _ParseLiveListWidgetState } } -class ParseLiveListElementWidget extends StatefulWidget { +class ParseLiveListElementWidget + extends StatefulWidget { const ParseLiveListElementWidget( {Key key, this.stream, @@ -174,8 +173,8 @@ class ParseLiveListElementWidget extends StatefulWidget { @required this.childBuilder}) : super(key: key); - final StreamGetter stream; - final DataGetter loadedData; + final sdk.StreamGetter stream; + final sdk.DataGetter loadedData; final Animation sizeFactor; final Duration duration; final ChildBuilder childBuilder; @@ -186,31 +185,32 @@ class ParseLiveListElementWidget extends StatefulWidget { } } -class _ParseLiveListElementWidgetState +class _ParseLiveListElementWidgetState extends State> with SingleTickerProviderStateMixin { _ParseLiveListElementWidgetState( - DataGetter loadedDataGetter, StreamGetter stream) { - _snapshot = ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); + sdk.DataGetter loadedDataGetter, sdk.StreamGetter stream) { + _snapshot = + sdk.ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); if (stream != null) { _streamSubscription = stream().listen( (T data) { if (widget != null) { setState(() { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); + _snapshot = sdk.ParseLiveListElementSnapshot(loadedData: data); }); } else { - _snapshot = ParseLiveListElementSnapshot(loadedData: data); + _snapshot = sdk.ParseLiveListElementSnapshot(loadedData: data); } }, onError: (Object error) { - if (error is ParseError) { + if (error is sdk.ParseError) { if (widget != null) { setState(() { - _snapshot = ParseLiveListElementSnapshot(error: error); + _snapshot = sdk.ParseLiveListElementSnapshot(error: error); }); } else { - _snapshot = ParseLiveListElementSnapshot(error: error); + _snapshot = sdk.ParseLiveListElementSnapshot(error: error); } } }, @@ -219,7 +219,7 @@ class _ParseLiveListElementWidgetState } } - ParseLiveListElementSnapshot _snapshot; + sdk.ParseLiveListElementSnapshot _snapshot; StreamSubscription _streamSubscription; diff --git a/pubspec.yaml b/packages/flutter/pubspec.yaml similarity index 79% rename from pubspec.yaml rename to packages/flutter/pubspec.yaml index 415a1cee1..fad78ea80 100644 --- a/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -1,4 +1,4 @@ -name: parse_server_sdk +name: parse_server_sdk_flutter description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) version: 1.0.28 homepage: https://github.com/phillwiggins/flutter_parse_sdk @@ -11,22 +11,18 @@ dependencies: flutter: sdk: flutter + parse_server_sdk: + path: ../dart/ + # Networking - web_socket_channel: ^1.1.0 connectivity: ^0.4.9+2 # only used in the flutter part #Database - sembast: ^2.4.7+6 - sembast_web: '>=1.0.0' - xxtea: ^2.0.3 shared_preferences: ^0.5.10 # only used in the flutter part # Utils path_provider: ^1.6.14 # only used in the flutter part - uuid: ^2.2.2 package_info: ^0.4.3 # only used in the flutter part - meta: ^1.1.8 - path: ^1.7.0 dev_dependencies: # Testing diff --git a/test/parse_client_configuration_test.dart b/packages/flutter/test/parse_client_configuration_test.dart similarity index 94% rename from test/parse_client_configuration_test.dart rename to packages/flutter/test/parse_client_configuration_test.dart index 87f9ab35c..f2de6b5cc 100644 --- a/test/parse_client_configuration_test.dart +++ b/packages/flutter/test/parse_client_configuration_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { diff --git a/test/parse_query_test.dart b/packages/flutter/test/parse_query_test.dart similarity index 95% rename from test/parse_query_test.dart rename to packages/flutter/test/parse_query_test.dart index 3a5133fbc..678c11e80 100644 --- a/test/parse_query_test.dart +++ b/packages/flutter/test/parse_query_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; class MockClient extends Mock implements ParseHTTPClient {} From d41596a5710535f702fea06eb47e7f916aee9e01 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 11 Sep 2020 10:47:57 +0200 Subject: [PATCH 10/33] Small changes and improvements (#441) * docs changes * remove flutter_test * CoreStore was implemented twice * implemented CoreStoreMemoryImp (default corestore for dart) * fix tests * Update .travis.yml --- .travis.yml | 6 +- README.md | 3 +- CHANGELOG.md => packages/dart/CHANGELOG.md | 1 + packages/dart/README.md | 9 +-- packages/dart/lib/parse_server_sdk.dart | 21 +++--- packages/dart/lib/src/data/core_store.dart | 31 -------- .../dart/lib/src/data/parse_core_data.dart | 9 +-- .../lib/src/storage/core_store_memory.dart | 75 +++++++++++++++++++ packages/dart/pubspec.yaml | 3 +- .../test/parse_client_configuration_test.dart | 29 +++++++ packages/dart/test/parse_query_test.dart | 43 +++++++++++ packages/flutter/CHANGELOG.md | 2 + packages/flutter/README.md | 5 +- 13 files changed, 175 insertions(+), 62 deletions(-) rename CHANGELOG.md => packages/dart/CHANGELOG.md (94%) delete mode 100644 packages/dart/lib/src/data/core_store.dart create mode 100644 packages/dart/lib/src/storage/core_store_memory.dart create mode 100644 packages/dart/test/parse_client_configuration_test.dart create mode 100644 packages/dart/test/parse_query_test.dart create mode 100644 packages/flutter/CHANGELOG.md diff --git a/.travis.yml b/.travis.yml index 13c0c03b0..a9fdeaa96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,10 @@ install: - flutter doctor script: - - flutter packages get - - flutter test --no-pub test/ + - (cd packages/dart && dart pub get) + - (cd packages/dart && dart test test/) + - (cd packages/flutter && flutter pub get) + - (cd packages/flutter && flutter test --no-pub test/) cache: directories: diff --git a/README.md b/README.md index cd73effc2..4942c6885 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -![Parse Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +![Parse Logo](https://parseplatform.org/img/logo.svg) + --- This repository contains packages that allow communication with a Parse Server, diff --git a/CHANGELOG.md b/packages/dart/CHANGELOG.md similarity index 94% rename from CHANGELOG.md rename to packages/dart/CHANGELOG.md index c17e5d3d5..6d36111d2 100644 --- a/CHANGELOG.md +++ b/packages/dart/CHANGELOG.md @@ -1,4 +1,5 @@ ## 1.0.28 +Spit this package: All flutter parts are now in [this](https://pub.dev/packages/parse_server_sdk_flutter) package! ## 1.0.27 User login / signUp / loginAnonymous delete SessionId stored in device before calling server diff --git a/packages/dart/README.md b/packages/dart/README.md index 04ff714f3..cfe6c71d9 100644 --- a/packages/dart/README.md +++ b/packages/dart/README.md @@ -1,4 +1,4 @@ -![Dart Logo](https://dart.dev/assets/shared/dart-logo-for-shares.png?2) ![Parse Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +![Parse Logo](https://parseplatform.org/img/logo.svg) ![Dart Logo](https://dart.dev/assets/shared/dart-logo-for-shares.png?2) **THIS README IS WORK IN PROGRESS** @@ -8,9 +8,6 @@ This is a Dart package that allows communication with a Parse Server, (https://p This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) -## Join in! -Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) - ## Getting Started To install, either add to your pubspec.yaml ```yml @@ -37,7 +34,7 @@ If you want to use secure storage or use the Flutter web/desktop SDK, please cha await Parse().initialize( keyParseApplicationId, keyParseServerUrl, - coreStore: await CoreStoreSembastImp.getInstance()); + coreStore: await CoreStoreSembastImp.getInstance("/data")); ``` It's possible to add other parameters to work with your instance of Parse Server:- @@ -51,7 +48,7 @@ It's possible to add other parameters to work with your instance of Parse Server liveQueryUrl: keyLiveQueryUrl, // Required if using LiveQuery autoSendSessionId: true, // Required for authentication and ACL securityContext: securityContext, // Again, required for some setups - coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB + coreStore: CoreStoreMemoryImp()); // Non persistent mode (default): Sdk will store everything in memmore instead of using Sembast as an internal DB. ``` diff --git a/packages/dart/lib/parse_server_sdk.dart b/packages/dart/lib/parse_server_sdk.dart index c5da87525..b0bf2b32b 100644 --- a/packages/dart/lib/parse_server_sdk.dart +++ b/packages/dart/lib/parse_server_sdk.dart @@ -19,17 +19,9 @@ import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:xxtea/xxtea.dart'; -part 'package:parse_server_sdk/src/data/core_store.dart'; -part 'package:parse_server_sdk/src/data/parse_subclass_handler.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_error_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_exception_response.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_builder.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_response_utils.dart'; -part 'package:parse_server_sdk/src/objects/response/parse_success_no_results.dart'; -part 'package:parse_server_sdk/src/storage/core_store_sem_impl.dart'; -part 'package:parse_server_sdk/src/storage/xxtea_codec.dart'; part 'src/base/parse_constants.dart'; part 'src/data/parse_core_data.dart'; +part 'src/data/parse_subclass_handler.dart'; part 'src/enums/parse_enum_api_rq.dart'; part 'src/network/parse_connectivity.dart'; part 'src/network/parse_http_client.dart'; @@ -52,14 +44,23 @@ part 'src/objects/parse_relation.dart'; part 'src/objects/parse_response.dart'; part 'src/objects/parse_session.dart'; part 'src/objects/parse_user.dart'; +part 'src/objects/response/parse_error_response.dart'; +part 'src/objects/response/parse_exception_response.dart'; +part 'src/objects/response/parse_response_builder.dart'; +part 'src/objects/response/parse_response_utils.dart'; +part 'src/objects/response/parse_success_no_results.dart'; +part 'src/storage/core_store.dart'; +part 'src/storage/core_store_memory.dart'; +part 'src/storage/core_store_sem_impl.dart'; +part 'src/storage/xxtea_codec.dart'; part 'src/utils/parse_date_format.dart'; part 'src/utils/parse_decoder.dart'; part 'src/utils/parse_encoder.dart'; part 'src/utils/parse_file_extensions.dart'; +part 'src/utils/parse_live_list.dart'; part 'src/utils/parse_logger.dart'; part 'src/utils/parse_login_helpers.dart'; part 'src/utils/parse_utils.dart'; -part 'src/utils/parse_live_list.dart'; class Parse { ParseCoreData data; diff --git a/packages/dart/lib/src/data/core_store.dart b/packages/dart/lib/src/data/core_store.dart deleted file mode 100644 index b7d2fe450..000000000 --- a/packages/dart/lib/src/data/core_store.dart +++ /dev/null @@ -1,31 +0,0 @@ -part of flutter_parse_sdk; - -abstract class CoreStore { - Future containsKey(String key); - - Future get(String key); - - Future getBool(String key); - - Future getInt(String key); - - Future getDouble(String key); - - Future getString(String key); - - Future> getStringList(String key); - - Future setBool(String key, bool value); - - Future setInt(String key, int value); - - Future setDouble(String key, double value); - - Future setString(String key, String value); - - Future setStringList(String key, List values); - - Future remove(String key); - - Future clear(); -} diff --git a/packages/dart/lib/src/data/parse_core_data.dart b/packages/dart/lib/src/data/parse_core_data.dart index 514b83dd5..de4fe75c3 100644 --- a/packages/dart/lib/src/data/parse_core_data.dart +++ b/packages/dart/lib/src/data/parse_core_data.dart @@ -35,14 +35,11 @@ class ParseCoreData { List liveListRetryIntervals, ParseConnectivityProvider connectivityProvider, String fileDirectory, - Stream appResumedStream, + Stream appResumedStream, }) async { _instance = ParseCoreData._init(appId, serverUrl); - assert(_instance.storage != null || store != null, - 'There is no CoreStore set.'); - - _instance.storage ??= store; + _instance.storage ??= store ?? CoreStoreMemoryImp(); if (debug != null) { _instance.debug = debug; @@ -98,7 +95,7 @@ class ParseCoreData { _instance.fileDirectory = fileDirectory; } - if(appResumedStream!= null){ + if (appResumedStream != null) { _instance.appResumedStream = appResumedStream; } } diff --git a/packages/dart/lib/src/storage/core_store_memory.dart b/packages/dart/lib/src/storage/core_store_memory.dart new file mode 100644 index 000000000..238c2ffd0 --- /dev/null +++ b/packages/dart/lib/src/storage/core_store_memory.dart @@ -0,0 +1,75 @@ +part of flutter_parse_sdk; + +class CoreStoreMemoryImp implements CoreStore { + static Map _data = {}; + + @override + Future clear() async { + _data = {}; + } + + @override + Future containsKey(String key) async { + return _data.containsKey(key); + } + + @override + Future get(String key) async { + return _data[key]; + } + + @override + Future getBool(String key) async { + return _data[key]; + } + + @override + Future getDouble(String key) async { + return _data[key]; + } + + @override + Future getInt(String key) async { + return _data[key]; + } + + @override + Future getString(String key) async { + return _data[key]; + } + + @override + Future> getStringList(String key) async { + return _data[key]; + } + + @override + Future remove(String key) async { + return _data.remove(key); + } + + @override + Future setBool(String key, bool value) async { + _data[key] = value; + } + + @override + Future setDouble(String key, double value) async { + _data[key] = value; + } + + @override + Future setInt(String key, int value) async { + _data[key] = value; + } + + @override + Future setString(String key, String value) async { + _data[key] = value; + } + + @override + Future setStringList(String key, List values) async { + _data[key] = values; + } +} diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml index 888de316e..da4f07621 100644 --- a/packages/dart/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -24,6 +24,5 @@ dependencies: dev_dependencies: # Testing - flutter_test: - sdk: flutter + test: ^1.15.3 mockito: ^4.1.1 diff --git a/packages/dart/test/parse_client_configuration_test.dart b/packages/dart/test/parse_client_configuration_test.dart new file mode 100644 index 000000000..f55f2ebf1 --- /dev/null +++ b/packages/dart/test/parse_client_configuration_test.dart @@ -0,0 +1,29 @@ +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:test/test.dart'; + +void main() { + test('testBuilder', () async { + await Parse().initialize('appId', 'serverUrl', + clientKey: 'clientKey', + liveQueryUrl: 'liveQueryUrl', + appName: 'appName', + appPackageName: 'somePackageName', + appVersion: 'someAppVersion', + masterKey: 'masterKey', + sessionId: 'sessionId', + fileDirectory: 'someDirectory', + debug: true,); + + expect(ParseCoreData().applicationId, 'appId'); + expect(ParseCoreData().serverUrl, 'serverUrl'); + expect(ParseCoreData().clientKey, 'clientKey'); + expect(ParseCoreData().liveQueryURL, 'liveQueryUrl'); + expect(ParseCoreData().appName, 'appName'); + expect(ParseCoreData().appPackageName, 'somePackageName'); + expect(ParseCoreData().appVersion, 'someAppVersion'); + expect(ParseCoreData().masterKey, 'masterKey'); + expect(ParseCoreData().sessionId, 'sessionId'); + expect(ParseCoreData().debug, true); + expect(ParseCoreData().fileDirectory, 'someDirectory'); + }); +} \ No newline at end of file diff --git a/packages/dart/test/parse_query_test.dart b/packages/dart/test/parse_query_test.dart new file mode 100644 index 000000000..7a8ccb563 --- /dev/null +++ b/packages/dart/test/parse_query_test.dart @@ -0,0 +1,43 @@ +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk/parse_server_sdk.dart'; +import 'package:test/test.dart'; + +class MockClient extends Mock implements ParseHTTPClient {} + +void main() { + group('queryBuilder', () { + test('whereRelatedTo', () async { + final MockClient client = MockClient(); + + await Parse().initialize( + 'appId', + 'https://test.parse.com', + debug: true, + // to prevent automatic detection + fileDirectory: 'someDirectory', + // to prevent automatic detection + appName: 'appName', + // to prevent automatic detection + appPackageName: 'somePackageName', + // to prevent automatic detection + appVersion: 'someAppVersion', + ); + + final QueryBuilder queryBuilder = + QueryBuilder(ParseObject('_User', client: client)); + queryBuilder.whereRelatedTo('likes', 'Post', '8TOXdXf3tz'); + + when(client.data).thenReturn(ParseCoreData()); + await queryBuilder.query(); + + final Uri result = verify(client.get(captureAny)).captured.single; + + expect(result.path, '/classes/_User'); + + final Uri expectedQuery = Uri( + query: + 'where={"\$relatedTo":{"object":{"__type":"Pointer","className":"Post","objectId":"8TOXdXf3tz"},"key":"likes"}}'); + expect(result.query, expectedQuery.query); + }); + }); +} diff --git a/packages/flutter/CHANGELOG.md b/packages/flutter/CHANGELOG.md new file mode 100644 index 000000000..57713cde8 --- /dev/null +++ b/packages/flutter/CHANGELOG.md @@ -0,0 +1,2 @@ +## 1.0.28 + diff --git a/packages/flutter/README.md b/packages/flutter/README.md index 800477861..2cc329d4b 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -1,14 +1,11 @@ -![Parse Logo](https://upload.wikimedia.org/wikipedia/commons/1/17/Google-flutter-logo.png)![Flutter Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +![Parse Logo](https://parseplatform.org/img/logo.svg) ![Flutter Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) ## Parse For Flutter! Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) -## Join in! -Want to get involved? Join our Slack channel and help out! (http://flutter-parse-sdk.slack.com) - ## Getting Started To install, either add to your pubspec.yaml ```yml From 623962073c6f766c88170a897598fa87c2165c8e Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 11 Sep 2020 14:22:10 +0200 Subject: [PATCH 11/33] Fix links (#443) * fix flutter logo * fixed flutter link --- README.md | 2 +- packages/flutter/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4942c6885..54a5ffd26 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ These are the available packages in this repository. | Plugin | Pub | explanation| |--------|-----|------------| | [parse_server_sdk](./packages/dart) | [![pub package](https://img.shields.io/pub/v/parse_server_sdk.svg)](https://pub.dev/packages/parse_server_sdk) | a dart package that lets you communicate with the parse server | -| [parse_server_sdk_flutter](./packages/dart) | [![pub package](https://img.shields.io/pub/v/parse_server_sdk_flutter.svg)](https://pub.dev/packages/parse_server_sdk_flutter) | a flutter package that lets you communicate with the parse server | +| [parse_server_sdk_flutter](./packages/flutter) | [![pub package](https://img.shields.io/pub/v/parse_server_sdk_flutter.svg)](https://pub.dev/packages/parse_server_sdk_flutter) | a flutter package that lets you communicate with the parse server | ### Author:- This project was authored by Phill Wiggins. You can contact me at phill.wiggins@gmail.com diff --git a/packages/flutter/README.md b/packages/flutter/README.md index 2cc329d4b..03e2094c5 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -1,5 +1,5 @@ -![Parse Logo](https://parseplatform.org/img/logo.svg) ![Flutter Logo](https://i2.wp.com/blog.openshift.com/wp-content/uploads/parse-server-logo-1.png?fit=200%2C200&ssl=1&resize=350%2C200) +![Parse Logo](https://parseplatform.org/img/logo.svg) ![Flutter Logo](https://upload.wikimedia.org/wikipedia/commons/1/17/Google-flutter-logo.png) ## Parse For Flutter! Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). From 6e0f0b463d64aeb362c5aadd13acb2d9c579c599 Mon Sep 17 00:00:00 2001 From: fischerscode Date: Sun, 13 Sep 2020 17:20:12 +0200 Subject: [PATCH 12/33] Fix infinite connecting on web --- packages/flutter/lib/parse_server_sdk.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/parse_server_sdk.dart b/packages/flutter/lib/parse_server_sdk.dart index c8d4d6088..96ff1f398 100644 --- a/packages/flutter/lib/parse_server_sdk.dart +++ b/packages/flutter/lib/parse_server_sdk.dart @@ -91,7 +91,9 @@ class Parse extends sdk.Parse parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, connectivityProvider: connectivityProvider ?? this, - fileDirectory: fileDirectory ?? (await getTemporaryDirectory()).path, + fileDirectory: fileDirectory ?? !sdk.parseIsWeb + ? (await getTemporaryDirectory()).path + : null, appResumedStream: appResumedStream ?? _appResumedStreamController.stream, ); } From 996c8c0e2cf7e554058ec82dfa4ab69a77784b86 Mon Sep 17 00:00:00 2001 From: fischerscode Date: Wed, 16 Sep 2020 13:11:12 +0200 Subject: [PATCH 13/33] delete old example folder --- example/.vscode/launch.json | 11 -- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes example/android/gradlew | 160 ------------------ 3 files changed, 171 deletions(-) delete mode 100644 example/.vscode/launch.json delete mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar delete mode 100755 example/android/gradlew diff --git a/example/.vscode/launch.json b/example/.vscode/launch.json deleted file mode 100644 index 5086a1da4..000000000 --- a/example/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Flutter Desktop Attach", - "request": "attach", - "observatoryUri": "http://127.0.0.1:52878/UWW_6_X9Y74=/", - "type": "dart" - } - ] -} \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/example/android/gradlew b/example/android/gradlew deleted file mode 100755 index 9d82f7891..000000000 --- a/example/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" From 056ae2a481d1c54a079beedcc7e87280901695fb Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Wed, 16 Sep 2020 17:29:25 +0200 Subject: [PATCH 14/33] fixing travis tests (#447) Should work again. --- .travis.yml | 6 +++--- packages/flutter/lib/parse_server_sdk.dart | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9fdeaa96..e1599d287 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,12 @@ dart: install: - git clone https://github.com/flutter/flutter.git -b stable --depth 1 - - export PATH=./flutter/bin:$PATH + - export PATH=~/build/parse-community/Parse-SDK-Flutter/flutter/bin:$PATH - flutter doctor script: - - (cd packages/dart && dart pub get) - - (cd packages/dart && dart test test/) + - (cd packages/dart && pub get) + - (cd packages/dart && pub run test) - (cd packages/flutter && flutter pub get) - (cd packages/flutter && flutter test --no-pub test/) diff --git a/packages/flutter/lib/parse_server_sdk.dart b/packages/flutter/lib/parse_server_sdk.dart index 96ff1f398..a0b26e1ee 100644 --- a/packages/flutter/lib/parse_server_sdk.dart +++ b/packages/flutter/lib/parse_server_sdk.dart @@ -91,9 +91,8 @@ class Parse extends sdk.Parse parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, connectivityProvider: connectivityProvider ?? this, - fileDirectory: fileDirectory ?? !sdk.parseIsWeb - ? (await getTemporaryDirectory()).path - : null, + fileDirectory: fileDirectory ?? + (!sdk.parseIsWeb ? (await getTemporaryDirectory()).path : null), appResumedStream: appResumedStream ?? _appResumedStreamController.stream, ); } From 6bbc1565e6ab79ef80b5c8e3b2964ebc9c639629 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 17 Sep 2020 14:42:29 +0200 Subject: [PATCH 15/33] Fixed throw in live list stream (#448) --- packages/dart/lib/src/utils/parse_live_list.dart | 11 +++++++---- packages/dart/lib/src/utils/parse_utils.dart | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/dart/lib/src/utils/parse_live_list.dart b/packages/dart/lib/src/utils/parse_live_list.dart index 827e27f6b..db4e3a1cc 100644 --- a/packages/dart/lib/src/utils/parse_live_list.dart +++ b/packages/dart/lib/src/utils/parse_live_list.dart @@ -406,11 +406,17 @@ class ParseLiveList { keyVarObjectId, _list[index].object.get(keyVarObjectId)) ..setLimit(1); final ParseResponse response = await queryBuilder.query(); + if (_list.isEmpty) { + yield* _createStreamError( + ParseError(message: 'ParseLiveList: _list is empty')); + return; + } if (response.success) { _list[index].object = response.results?.first; } else { _list[index].object = null; - throw response.error; + yield* _createStreamError(response.error); + return; } } // just for testing @@ -732,7 +738,6 @@ class ParseLiveListDeleteEvent typedef StreamGetter = Stream Function(); typedef DataGetter = T Function(); - class ParseLiveListElementSnapshot { ParseLiveListElementSnapshot({this.loadedData, this.error}); @@ -743,5 +748,3 @@ class ParseLiveListElementSnapshot { bool get failed => error != null; } - - diff --git a/packages/dart/lib/src/utils/parse_utils.dart b/packages/dart/lib/src/utils/parse_utils.dart index 0fa7b4c2b..e39ff5d03 100644 --- a/packages/dart/lib/src/utils/parse_utils.dart +++ b/packages/dart/lib/src/utils/parse_utils.dart @@ -84,3 +84,7 @@ Future batchRequest( return handleException(e, ParseApiRQ.batch, debug, 'parse_utils'); } } + +Stream _createStreamError(Object error) async* { + throw error; +} From b9f51a644a240374061856bdca78bbcb5ddce01b Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 17 Sep 2020 16:11:37 +0200 Subject: [PATCH 16/33] ParseUser: store password in field (#446) * store password in field Potential fix for https://github.com/parse-community/Parse-SDK-Flutter/issues/444 * remove password from clone --- packages/dart/lib/src/objects/parse_user.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/dart/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart index 55c822e71..e00af3161 100644 --- a/packages/dart/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -11,7 +11,7 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Requires [String] username, [String] password. [String] email address /// is required as well to create a full new user object on ParseServer. Only /// username and password is required to login - ParseUser(String username, String password, String emailAddress, + ParseUser(String username, this.password, String emailAddress, {String sessionToken, bool debug, ParseHTTPClient client}) : super(keyClassUser) { _debug = isDebugEnabled(objectLevelDebug: debug); @@ -21,7 +21,6 @@ class ParseUser extends ParseObject implements ParseCloneable { securityContext: ParseCoreData().securityContext); this.username = username; - this.password = password; this.emailAddress = emailAddress; this.sessionToken = sessionToken; } @@ -29,7 +28,7 @@ class ParseUser extends ParseObject implements ParseCloneable { ParseUser.forQuery() : super(keyClassUser); ParseUser.clone(Map map) - : this(map[keyVarUsername], map[keyVarPassword], map[keyVarEmail]); + : this(map[keyVarUsername], null, map[keyVarEmail]); @override dynamic clone(Map map) => @@ -40,6 +39,8 @@ class ParseUser extends ParseObject implements ParseCloneable { static const String keyEmailAddress = 'email'; static const String path = '$keyEndPointClasses$keyClassUser'; + String password; + Map get acl => super.get>(keyVarAcl); set acl(Map acl) => @@ -54,10 +55,6 @@ class ParseUser extends ParseObject implements ParseCloneable { set username(String username) => set(keyVarUsername, username); - String get password => super.get(keyVarPassword); - - set password(String password) => set(keyVarPassword, password); - String get emailAddress => super.get(keyVarEmail); set emailAddress(String emailAddress) => From fca5cb8ffef8088f82390a4d20413e060fda4422 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 25 Sep 2020 15:25:30 +0200 Subject: [PATCH 17/33] fix for issue #456 (#457) Added @BaranMichal25 fix for #456. https://github.com/parse-community/Parse-SDK-Flutter/issues/456#issue-707407672 --- packages/dart/lib/src/objects/parse_user.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dart/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart index e00af3161..6df715954 100644 --- a/packages/dart/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -149,6 +149,7 @@ class ParseUser extends ParseObject implements ParseCloneable { } final Map bodyData = _getObjectData(); + bodyData[keyVarPassword] = password; final Uri url = getSanitisedUri(_client, '$path'); final String body = json.encode(bodyData); _saveChanges(); From fd52b84bddb7e603803957d1d0bab763dd8463d4 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 2 Oct 2020 13:44:34 +0200 Subject: [PATCH 18/33] LiveList: preloadedColumns (#458) * LiveList: added preloadedColumns * LiveList: fixed bug with lazyloading=false * formatting * Add README section for flutter --- .../dart/lib/src/utils/parse_live_list.dart | 81 +++++++++++-------- packages/flutter/README.md | 32 ++++++++ .../lib/src/utils/parse_live_list.dart | 28 +++++-- 3 files changed, 101 insertions(+), 40 deletions(-) diff --git a/packages/dart/lib/src/utils/parse_live_list.dart b/packages/dart/lib/src/utils/parse_live_list.dart index db4e3a1cc..fd29f9235 100644 --- a/packages/dart/lib/src/utils/parse_live_list.dart +++ b/packages/dart/lib/src/utils/parse_live_list.dart @@ -2,34 +2,47 @@ part of flutter_parse_sdk; // ignore_for_file: invalid_use_of_protected_member class ParseLiveList { - ParseLiveList._(this._query, this._listeningIncludes, this._lazyLoading) { + ParseLiveList._(this._query, this._listeningIncludes, this._lazyLoading, + {List preloadedColumns = const []}) + : _preloadedColumns = preloadedColumns { _debug = isDebugEnabled(); } static Future> create( - QueryBuilder _query, - {bool listenOnAllSubItems, - List listeningIncludes, - bool lazyLoading = true}) { + QueryBuilder _query, { + bool listenOnAllSubItems, + List listeningIncludes, + bool lazyLoading = true, + List preloadedColumns, + }) { final ParseLiveList parseLiveList = ParseLiveList._( - _query, - listenOnAllSubItems == true - ? _toIncludeMap( - _query.limiters['include']?.toString()?.split(',') ?? - []) - : _toIncludeMap(listeningIncludes ?? []), - lazyLoading); + _query, + listenOnAllSubItems == true + ? _toIncludeMap( + _query.limiters['include']?.toString()?.split(',') ?? []) + : _toIncludeMap(listeningIncludes ?? []), + lazyLoading, + preloadedColumns: preloadedColumns, + ); return parseLiveList._init().then((_) { return parseLiveList; }); } + final QueryBuilder _query; + //The included Items, where LiveList should look for updates. + final Map _listeningIncludes; + final bool _lazyLoading; + final List _preloadedColumns; + List> _list = List>(); StreamController> _eventStreamController; int _nextID = 0; bool _debug; + int get nextID => _nextID++; + /// is object1 listed after object2? /// can return null bool after(T object1, T object2) { @@ -84,14 +97,6 @@ class ParseLiveList { return null; } - int get nextID => _nextID++; - - final QueryBuilder _query; - //The included Items, where LiveList should look for updates. - final Map _listeningIncludes; - - final bool _lazyLoading; - int get size { return _list.length; } @@ -128,17 +133,17 @@ class ParseLiveList { if (_debug) print('ParseLiveList: lazyLoading is ${_lazyLoading ? 'on' : 'off'}'); if (_lazyLoading) { - if (query.limiters.containsKey('order')) { - query.keysToReturn( - query.limiters['order'].toString().split(',').map((String string) { - if (string.startsWith('-')) { - return string.substring(1); - } - return string; - }).toList()); - } else { - query.keysToReturn(List()); - } + final List keys = _preloadedColumns?.toList() ?? []; + if (_lazyLoading && query.limiters.containsKey('order')) + keys.addAll( + query.limiters['order'].toString().split(',').map((String string) { + if (string.startsWith('-')) { + return string.substring(1); + } + return string; + }), + ); + query.keysToReturn(keys); } return await query.query(); } @@ -449,6 +454,13 @@ class ParseLiveList { return null; } + T getPreLoadedAt(int index) { + if (index < _list.length) { + return _list[index].object; + } + return null; + } + void dispose() { if (_liveQuerySubscription != null) { LiveQuery().client.unSubscribe(_liveQuerySubscription); @@ -739,12 +751,17 @@ typedef StreamGetter = Stream Function(); typedef DataGetter = T Function(); class ParseLiveListElementSnapshot { - ParseLiveListElementSnapshot({this.loadedData, this.error}); + ParseLiveListElementSnapshot( + {this.loadedData, this.error, this.preLoadedData}); final T loadedData; + final T preLoadedData; + final ParseError error; bool get hasData => loadedData != null; + bool get hasPreLoadedData => preLoadedData != null; + bool get failed => error != null; } diff --git a/packages/flutter/README.md b/packages/flutter/README.md index 03e2094c5..7fd06cba4 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -597,6 +597,38 @@ To activate listening for updates on all included objects, add `listenOnAllSubIt If you want ParseLiveList to listen for updates on only some sub-objects, use `listeningIncludes: const [/*all the included sub-objects*/]` instead. Just as QueryBuilder, ParseLiveList supports nested sub-objects too. +### Lazy loading +By default, ParseLiveList lazy loads the content. +You can avoid that by setting `lazyLoading: false`. +In case you want to use lazyLoading but you need some columns to be preloaded, you can provide a list of `preloadedColumns`. +Preloading fields of a pointer is supported by using the dot-notation. +You can access the preloaded data is stored in the `preLoadedData` field of the `ParseLiveListElementSnapshot`. +```dart +ParseLiveListWidget( + query: query, + lazyLoading: true, + preloadedColumns: ["test1", "sender.username"], + childBuilder: + (BuildContext context, ParseLiveListElementSnapshot snapshot) { + if (snapshot.failed) { + return const Text('something went wrong!'); + } else if (snapshot.hasData) { + return ListTile( + title: Text( + snapshot.loadedData.get("text"), + ), + ); + } else { + return ListTile( + title: Text( + "loading comment from: ${snapshot.preLoadedData?.get("sender")?.get("username")}", + ), + ); + } + }, +); +``` + **NOTE:** To use this features you have to enable [Live Queries](#live-queries) first. ## Users diff --git a/packages/flutter/lib/src/utils/parse_live_list.dart b/packages/flutter/lib/src/utils/parse_live_list.dart index a4674dac5..d80efed77 100644 --- a/packages/flutter/lib/src/utils/parse_live_list.dart +++ b/packages/flutter/lib/src/utils/parse_live_list.dart @@ -21,6 +21,7 @@ class ParseLiveListWidget extends StatefulWidget { this.listenOnAllSubItems, this.listeningIncludes, this.lazyLoading = true, + this.preloadedColumns, }) : super(key: key); final sdk.QueryBuilder query; @@ -42,6 +43,7 @@ class ParseLiveListWidget extends StatefulWidget { final List listeningIncludes; final bool lazyLoading; + final List preloadedColumns; @override _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( @@ -50,6 +52,7 @@ class ParseLiveListWidget extends StatefulWidget { listenOnAllSubItems: listenOnAllSubItems, listeningIncludes: listeningIncludes, lazyLoading: lazyLoading, + preloadedColumns: preloadedColumns, ); static Widget defaultChildBuilder( @@ -79,12 +82,14 @@ class _ParseLiveListWidgetState @required this.removedItemBuilder, bool listenOnAllSubItems, List listeningIncludes, - bool lazyLoading = true}) { + bool lazyLoading = true, + List preloadedColumns}) { sdk.ParseLiveList.create( query, listenOnAllSubItems: listenOnAllSubItems, listeningIncludes: listeningIncludes, lazyLoading: lazyLoading, + preloadedColumns: preloadedColumns, ).then((sdk.ParseLiveList value) { setState(() { _liveList = value; @@ -107,6 +112,7 @@ class _ParseLiveListWidgetState sizeFactor: animation, duration: widget.duration, loadedData: () => event.object, + preLoadedData: () => event.object, ), duration: widget.duration); } @@ -146,6 +152,7 @@ class _ParseLiveListWidgetState _liveList?.getIdentifier(index) ?? '_NotFound'), stream: () => _liveList?.getAt(index), loadedData: () => _liveList?.getLoadedAt(index), + preLoadedData: () => _liveList?.getPreLoadedAt(index), sizeFactor: animation, duration: widget.duration, childBuilder: @@ -168,6 +175,7 @@ class ParseLiveListElementWidget {Key key, this.stream, this.loadedData, + this.preLoadedData, @required this.sizeFactor, @required this.duration, @required this.childBuilder}) @@ -175,32 +183,36 @@ class ParseLiveListElementWidget final sdk.StreamGetter stream; final sdk.DataGetter loadedData; + final sdk.DataGetter preLoadedData; final Animation sizeFactor; final Duration duration; final ChildBuilder childBuilder; @override _ParseLiveListElementWidgetState createState() { - return _ParseLiveListElementWidgetState(loadedData, stream); + return _ParseLiveListElementWidgetState( + loadedData, preLoadedData, stream); } } class _ParseLiveListElementWidgetState extends State> with SingleTickerProviderStateMixin { - _ParseLiveListElementWidgetState( - sdk.DataGetter loadedDataGetter, sdk.StreamGetter stream) { - _snapshot = - sdk.ParseLiveListElementSnapshot(loadedData: loadedDataGetter()); + _ParseLiveListElementWidgetState(sdk.DataGetter loadedDataGetter, + sdk.DataGetter preLoadedDataGetter, sdk.StreamGetter stream) { + _snapshot = sdk.ParseLiveListElementSnapshot( + loadedData: loadedDataGetter(), preLoadedData: preLoadedDataGetter()); if (stream != null) { _streamSubscription = stream().listen( (T data) { if (widget != null) { setState(() { - _snapshot = sdk.ParseLiveListElementSnapshot(loadedData: data); + _snapshot = sdk.ParseLiveListElementSnapshot( + loadedData: data, preLoadedData: data); }); } else { - _snapshot = sdk.ParseLiveListElementSnapshot(loadedData: data); + _snapshot = sdk.ParseLiveListElementSnapshot( + loadedData: data, preLoadedData: data); } }, onError: (Object error) { From 74c4845c8f95640811118c11516824a0e3186b62 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 2 Oct 2020 15:17:08 +0200 Subject: [PATCH 19/33] creat migration guide for version 1.0.28 (#445) * Create migrate-1-0-28.md * Update migrate-1-0-28.md * Update migrate-1-0-28.md * Update migrate-1-0-28.md * Merge from upstream/release/1.0.28 * fix typos * added "changed network library" to migration guide --- docs/migrate-1-0-28.md | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/migrate-1-0-28.md diff --git a/docs/migrate-1-0-28.md b/docs/migrate-1-0-28.md new file mode 100644 index 000000000..b6ae4fda4 --- /dev/null +++ b/docs/migrate-1-0-28.md @@ -0,0 +1,49 @@ +# Migrate your Flutter application to version 1.0.28 + +Starting with version 1.0.28, this repository is now separated in a pure dart (parse_server_sdk) and a flutter package (parse_server_sdk_flutter). +This was done in order to provide a dart package for the parse-server, while keeping maintenance simple. +You can find both packages in the package directory. + +### 1. pubspec.yaml +In your projects pubspec.yaml at the dependencies section, you have to change +``` +dependencies: + parse_server_sdk: ^1.0.27 +``` +to +``` +dependencies: + parse_server_sdk_flutter: ^1.0.28 +``` +This is the current released version of the parse_server_sdk_flutter package: [![pub package](https://img.shields.io/pub/v/parse_server_sdk_flutter.svg)](https://pub.dev/packages/parse_server_sdk_flutter) + +### 2. imports +As the package name changed, you have to change +``` +import 'package:parse_server_sdk/parse_server_sdk.dart'; +``` + to +``` +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +``` +in every file. + +It is recommended to do so by the replacement feature of your IDE. + +### optional: provide app information on web +As flutter web is now in beta, this SDK aims to be web compatible. +But there are some parts completely different on web. For example, the wep-app cant determine it's name, version or packagename. +That's why you should provide this information on web. +```dart +Parse().initialize( + ... + appName: kIsWeb ? "MyApplication" : null, + appVersion: kIsWeb ? "Version 1" : null, + appPackageName: kIsWeb ? "com.example.myapplication" : null, +); +``` + +### changed network library +In order to provide a `ProgressCallback` for heavy file operations, +the network library was switched from [http](https://pub.dev/packages/http) to [dio](https://pub.dev/packages/dio). +There should be no breaking changes regarding this change, except if you are overriding the `ParseHTTPClient`. \ No newline at end of file From 6e01d28020c80142984b14c7e7d1edef7016d9fb Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 8 Oct 2020 13:26:33 +0200 Subject: [PATCH 20/33] Move ParseHTTPClient to Dio (#459) * Merged manually from branch dio * fixing nullpointer * Fix batch operations Maybe dio changes the toString behaviour. * use mine * keep content-type * fix file upload issue * fixed file uppload on web * performance improvement in parse_file_web * fix for issue #456 (#457) Added @BaranMichal25 fix for #456. https://github.com/parse-community/Parse-SDK-Flutter/issues/456#issue-707407672 * fix flutter test * fix crash when debug=true * fix dart test * add ProgressCallback in README * added progressCallback example --- packages/dart/README.md | 7 +- packages/dart/lib/parse_server_sdk.dart | 11 +- .../dart/lib/src/base/parse_constants.dart | 2 +- .../dart/lib/src/network/dio-options.dart | 34 ++++++ .../lib/src/network/http_client_adapter.dart | 2 + .../network/http_client_adapter_native.dart | 14 +++ .../src/network/http_client_adapter_web.dart | 10 ++ .../lib/src/network/parse_http_client.dart | 56 +++++---- .../dart/lib/src/network/parse_query.dart | 8 +- .../dart/lib/src/objects/parse_config.dart | 4 +- packages/dart/lib/src/objects/parse_file.dart | 28 +++-- .../dart/lib/src/objects/parse_file_base.dart | 4 +- .../dart/lib/src/objects/parse_file_web.dart | 29 +++-- .../dart/lib/src/objects/parse_function.dart | 10 +- .../lib/src/objects/parse_installation.dart | 9 +- .../dart/lib/src/objects/parse_object.dart | 33 ++++-- .../dart/lib/src/objects/parse_session.dart | 3 +- packages/dart/lib/src/objects/parse_user.dart | 106 +++++++++++------- .../response/parse_error_response.dart | 7 +- .../response/parse_response_builder.dart | 10 +- .../response/parse_response_utils.dart | 12 +- .../lib/src/utils/parse_file_extensions.dart | 4 + packages/dart/lib/src/utils/parse_logger.dart | 30 ++--- packages/dart/lib/src/utils/parse_utils.dart | 3 +- packages/dart/pubspec.yaml | 3 +- packages/dart/test/parse_query_test.dart | 3 +- packages/flutter/README.md | 1 + packages/flutter/pubspec.yaml | 2 +- packages/flutter/test/parse_query_test.dart | 3 +- 29 files changed, 296 insertions(+), 152 deletions(-) create mode 100644 packages/dart/lib/src/network/dio-options.dart create mode 100644 packages/dart/lib/src/network/http_client_adapter.dart create mode 100644 packages/dart/lib/src/network/http_client_adapter_native.dart create mode 100644 packages/dart/lib/src/network/http_client_adapter_web.dart diff --git a/packages/dart/README.md b/packages/dart/README.md index cfe6c71d9..625b4690f 100644 --- a/packages/dart/README.md +++ b/packages/dart/README.md @@ -782,7 +782,7 @@ These classes are used by default to represent files, but you can also build you Have a look at the example application for a small (non web) example. - +When uploading or downloading a file, you can use the `progressCallback`-parameter to track the progress of the http request. ```dart //A short example for showing an image from a ParseFileBase Widget buildImage(ParseFileBase image){ @@ -820,7 +820,10 @@ someParseObject.set("image", parseFile); //This saves the ParseObject as well as all of its children, and the ParseFileBase is such a child. await someParseObject.save(); ``` - +```dart +//progressCallback example +file.upload(progressCallback: (int count, int total) => print("$count of $total")); +``` ## Other Features of this library Main: * Installation (View the example application) diff --git a/packages/dart/lib/parse_server_sdk.dart b/packages/dart/lib/parse_server_sdk.dart index b0bf2b32b..7681b39ab 100644 --- a/packages/dart/lib/parse_server_sdk.dart +++ b/packages/dart/lib/parse_server_sdk.dart @@ -6,9 +6,11 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:http/http.dart'; -import 'package:http/io_client.dart'; +import 'package:dio/dio.dart' hide Options; +import 'package:dio/dio.dart' as dio show Options; import 'package:meta/meta.dart'; +import 'package:mime_type/mime_type.dart'; +import 'package:parse_server_sdk/src/network/http_client_adapter.dart'; import 'package:parse_server_sdk/src/network/parse_websocket.dart' as parse_web_socket; import 'package:path/path.dart' as path; @@ -23,6 +25,7 @@ part 'src/base/parse_constants.dart'; part 'src/data/parse_core_data.dart'; part 'src/data/parse_subclass_handler.dart'; part 'src/enums/parse_enum_api_rq.dart'; +part 'src/network/dio-options.dart'; part 'src/network/parse_connectivity.dart'; part 'src/network/parse_http_client.dart'; part 'src/network/parse_live_query.dart'; @@ -150,8 +153,8 @@ class Parse { const ParseApiRQ type = ParseApiRQ.healthCheck; try { - final Response response = - await _client.get('${ParseCoreData().serverUrl}$keyEndPointHealth'); + final Response response = await _client + .get('${ParseCoreData().serverUrl}$keyEndPointHealth'); parseResponse = handleResponse(null, response, type, _debug, className); } on Exception catch (e) { diff --git a/packages/dart/lib/src/base/parse_constants.dart b/packages/dart/lib/src/base/parse_constants.dart index b645de916..25fe69010 100644 --- a/packages/dart/lib/src/base/parse_constants.dart +++ b/packages/dart/lib/src/base/parse_constants.dart @@ -46,7 +46,7 @@ const String keyHeaderSessionToken = 'X-Parse-Session-Token'; const String keyHeaderRevocableSession = 'X-Parse-Revocable-Session'; const String keyHeaderUserAgent = 'user-agent'; const String keyHeaderApplicationId = 'X-Parse-Application-Id'; -const String keyHeaderContentType = 'Content-Type'; +const String keyHeaderContentType = Headers.contentTypeHeader; const String keyHeaderContentTypeJson = 'application/json'; const String keyHeaderMasterKey = 'X-Parse-Master-Key'; const String keyHeaderClientKey = 'X-Parse-Client-Key'; diff --git a/packages/dart/lib/src/network/dio-options.dart b/packages/dart/lib/src/network/dio-options.dart new file mode 100644 index 000000000..cf20dcc9b --- /dev/null +++ b/packages/dart/lib/src/network/dio-options.dart @@ -0,0 +1,34 @@ +part of flutter_parse_sdk; + +class Options extends dio.Options { + Options({ + String method, + int sendTimeout, + int receiveTimeout, + Map extra, + Map headers, + ResponseType responseType, + String contentType, + ValidateStatus validateStatus, + bool receiveDataWhenStatusError, + bool followRedirects, + int maxRedirects, + RequestEncoder requestEncoder, + ResponseDecoder responseDecoder, + }) : super( + method: method, + sendTimeout: sendTimeout, + receiveTimeout: receiveTimeout, + extra: extra, + headers: headers, + responseType: responseType, + contentType: contentType ?? + (headers ?? {})[Headers.contentTypeHeader], + validateStatus: validateStatus, + receiveDataWhenStatusError: receiveDataWhenStatusError, + followRedirects: followRedirects, + maxRedirects: maxRedirects, + requestEncoder: requestEncoder, + responseDecoder: responseDecoder, + ); +} diff --git a/packages/dart/lib/src/network/http_client_adapter.dart b/packages/dart/lib/src/network/http_client_adapter.dart new file mode 100644 index 000000000..9d09a3c14 --- /dev/null +++ b/packages/dart/lib/src/network/http_client_adapter.dart @@ -0,0 +1,2 @@ +export 'http_client_adapter_native.dart' + if (dart.library.js) 'http_client_adapter_web.dart'; diff --git a/packages/dart/lib/src/network/http_client_adapter_native.dart b/packages/dart/lib/src/network/http_client_adapter_native.dart new file mode 100644 index 000000000..c5f87a175 --- /dev/null +++ b/packages/dart/lib/src/network/http_client_adapter_native.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:dio/adapter.dart'; +import 'package:dio/dio.dart'; + +HttpClientAdapter createHttpClientAdapter(SecurityContext securityContext) { + final DefaultHttpClientAdapter defaultHttpClientAdapter = + DefaultHttpClientAdapter(); + + if (securityContext != null) + defaultHttpClientAdapter.onHttpClientCreate = + (HttpClient client) => HttpClient(context: securityContext); + return defaultHttpClientAdapter; +} diff --git a/packages/dart/lib/src/network/http_client_adapter_web.dart b/packages/dart/lib/src/network/http_client_adapter_web.dart new file mode 100644 index 000000000..e7158e0de --- /dev/null +++ b/packages/dart/lib/src/network/http_client_adapter_web.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +import 'package:dio/adapter_browser.dart'; +import 'package:dio/dio.dart'; + +HttpClientAdapter createHttpClientAdapter(SecurityContext securityContext) { + final BrowserHttpClientAdapter browserHttpClientAdapter = + BrowserHttpClientAdapter(); + return browserHttpClientAdapter; +} diff --git a/packages/dart/lib/src/network/parse_http_client.dart b/packages/dart/lib/src/network/parse_http_client.dart index b577f4849..29c293477 100644 --- a/packages/dart/lib/src/network/parse_http_client.dart +++ b/packages/dart/lib/src/network/parse_http_client.dart @@ -1,14 +1,13 @@ part of flutter_parse_sdk; /// Creates a custom version of HTTP Client that has Parse Data Preset -class ParseHTTPClient extends BaseClient { +class ParseHTTPClient with DioMixin implements Dio { ParseHTTPClient({bool sendSessionId = false, SecurityContext securityContext}) - : _sendSessionId = sendSessionId, - _client = securityContext != null - ? IOClient(HttpClient(context: securityContext)) - : Client(); + : _sendSessionId = sendSessionId { + options = BaseOptions(); + httpClientAdapter = createHttpClientAdapter(securityContext); + } - final Client _client; final bool _sendSessionId; final String _userAgent = '$keyLibraryName $keySdkVersion'; ParseCoreData data = ParseCoreData(); @@ -16,31 +15,48 @@ class ParseHTTPClient extends BaseClient { /// Overrides the call method for HTTP Client and adds custom headers @override - Future send(BaseRequest request) { + Future> request( + String path, { + dynamic data, + Map queryParameters, + CancelToken cancelToken, + dio.Options options, + ProgressCallback onSendProgress, + ProgressCallback onReceiveProgress, + }) { + options ??= Options(); if (!identical(0, 0.0)) { - request.headers[keyHeaderUserAgent] = _userAgent; + options.headers[keyHeaderUserAgent] = _userAgent; } - request.headers[keyHeaderApplicationId] = data.applicationId; + options.headers[keyHeaderApplicationId] = this.data.applicationId; if ((_sendSessionId == true) && - (data.sessionId != null) && - (request.headers[keyHeaderSessionToken] == null)) - request.headers[keyHeaderSessionToken] = data.sessionId; + (this.data.sessionId != null) && + (options.headers[keyHeaderSessionToken] == null)) + options.headers[keyHeaderSessionToken] = this.data.sessionId; - if (data.clientKey != null) - request.headers[keyHeaderClientKey] = data.clientKey; - if (data.masterKey != null) - request.headers[keyHeaderMasterKey] = data.masterKey; + if (this.data.clientKey != null) + options.headers[keyHeaderClientKey] = this.data.clientKey; + if (this.data.masterKey != null) + options.headers[keyHeaderMasterKey] = this.data.masterKey; /// If developer wants to add custom headers, extend this class and add headers needed. if (additionalHeaders != null && additionalHeaders.isNotEmpty) { additionalHeaders - .forEach((String key, String value) => request.headers[key] = value); + .forEach((String key, String value) => options.headers[key] = value); } - if (data.debug) { - logCUrl(request); + if (this.data.debug) { + logCUrl(options, data, path); } - return _client.send(request); + return super.request( + path, + data: data, + queryParameters: queryParameters, + cancelToken: cancelToken, + options: options, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); } } diff --git a/packages/dart/lib/src/network/parse_query.dart b/packages/dart/lib/src/network/parse_query.dart index 9aca4aa3e..497fe928b 100644 --- a/packages/dart/lib/src/network/parse_query.dart +++ b/packages/dart/lib/src/network/parse_query.dart @@ -343,8 +343,12 @@ class QueryBuilder { /// Finishes the query and calls the server /// /// Make sure to call this after defining your queries - Future query() async { - return object.query(buildQuery()); + Future query( + {ProgressCallback progressCallback}) async { + return object.query( + buildQuery(), + progressCallback: progressCallback, + ); } Future distinct( diff --git a/packages/dart/lib/src/objects/parse_config.dart b/packages/dart/lib/src/objects/parse_config.dart index 8f7dedf16..1c2feb28b 100644 --- a/packages/dart/lib/src/objects/parse_config.dart +++ b/packages/dart/lib/src/objects/parse_config.dart @@ -16,7 +16,7 @@ class ParseConfig extends ParseObject { Future getConfigs() async { try { final String uri = '${ParseCoreData().serverUrl}/config'; - final Response result = await _client.get(uri); + final Response result = await _client.get(uri); return handleResponse( this, result, ParseApiRQ.getConfigs, _debug, parseClassName); } on Exception catch (e) { @@ -29,7 +29,7 @@ class ParseConfig extends ParseObject { try { final String uri = '${ParseCoreData().serverUrl}/config'; final String body = '{\"params\":{\"$key\": \"${parseEncode(value)}\"}}'; - final Response result = await _client.put(uri, body: body); + final Response result = await _client.put(uri, data: body); return handleResponse( this, result, ParseApiRQ.addConfig, _debug, parseClassName); } on Exception catch (e) { diff --git a/packages/dart/lib/src/objects/parse_file.dart b/packages/dart/lib/src/objects/parse_file.dart index a149839df..76349ec41 100644 --- a/packages/dart/lib/src/objects/parse_file.dart +++ b/packages/dart/lib/src/objects/parse_file.dart @@ -40,22 +40,26 @@ class ParseFile extends ParseFileBase { } @override - Future download() async { + Future download({ProgressCallback progressCallback}) async { if (url == null) { return this; } file = File('${ParseCoreData().fileDirectory}/$name'); await file.create(); - final Response response = await _client.get(url); - await file.writeAsBytes(response.bodyBytes); + final Response> response = await _client.get>( + url, + options: Options(responseType: ResponseType.bytes), + onReceiveProgress: progressCallback, + ); + await file.writeAsBytes(response.data); return this; } /// Uploads a file to Parse Server @override - Future upload() async { + Future upload({ProgressCallback progressCallback}) async { if (saved) { //Creates a Fake Response to return the correct result final Map response = { @@ -64,23 +68,25 @@ class ParseFile extends ParseFileBase { }; return handleResponse( this, - Response(json.encode(response), 201), + Response(data: json.encode(response), statusCode: 201), ParseApiRQ.upload, _debug, parseClassName); } - final String ext = path.extension(file.path).replaceAll('.', ''); final Map headers = { - HttpHeaders.contentTypeHeader: getContentType(ext) + HttpHeaders.contentTypeHeader: mime(file.path) ?? 'application/octet-stream', }; try { final String uri = _client.data.serverUrl + '$_path'; - final List body = await file.readAsBytes(); - final Response response = - await _client.post(uri, headers: headers, body: body); + final Response response = await _client.post( + uri, + options: Options(headers: headers), + data: file.openRead(), + onSendProgress: progressCallback, + ); if (response.statusCode == 201) { - final Map map = json.decode(response.body); + final Map map = json.decode(response.data); url = map['url'].toString(); name = map['name'].toString(); } diff --git a/packages/dart/lib/src/objects/parse_file_base.dart b/packages/dart/lib/src/objects/parse_file_base.dart index e565b92a7..4ca208259 100644 --- a/packages/dart/lib/src/objects/parse_file_base.dart +++ b/packages/dart/lib/src/objects/parse_file_base.dart @@ -41,7 +41,7 @@ abstract class ParseFileBase extends ParseObject { } /// Uploads a file to Parse Server - Future upload(); + Future upload({ProgressCallback progressCallback}); - Future download(); + Future download({ProgressCallback progressCallback}); } diff --git a/packages/dart/lib/src/objects/parse_file_web.dart b/packages/dart/lib/src/objects/parse_file_web.dart index f17369a96..323fff54d 100644 --- a/packages/dart/lib/src/objects/parse_file_web.dart +++ b/packages/dart/lib/src/objects/parse_file_web.dart @@ -18,19 +18,23 @@ class ParseWebFile extends ParseFileBase { Uint8List file; @override - Future download() async { + Future download({ProgressCallback progressCallback}) async { if (url == null) { return this; } - final Response response = await _client.get(url); - file = response.bodyBytes; + final Response> response = await _client.get>( + url, + options: Options(responseType: ResponseType.bytes), + onReceiveProgress: progressCallback, + ); + file = response.data; return this; } @override - Future upload() async { + Future upload({ProgressCallback progressCallback}) async { if (saved) { //Creates a Fake Response to return the correct result final Map response = { @@ -39,23 +43,26 @@ class ParseWebFile extends ParseFileBase { }; return handleResponse( this, - Response(json.encode(response), 201), + Response(data: json.encode(response), statusCode: 201), ParseApiRQ.upload, _debug, parseClassName); } final Map headers = { - HttpHeaders.contentTypeHeader: url ?? name != null - ? getContentType(path.extension(url ?? name)) - : 'text/plain' + HttpHeaders.contentTypeHeader: + mime(url ?? name) ?? 'application/octet-stream', }; try { final String uri = _client.data.serverUrl + '$_path'; - final Response response = - await _client.post(uri, headers: headers, body: file); + final Response response = await _client.post( + uri, + options: Options(headers: headers), + data: Stream>.fromIterable(>[file]), + onSendProgress: progressCallback, + ); if (response.statusCode == 201) { - final Map map = json.decode(response.body); + final Map map = json.decode(response.data); url = map['url'].toString(); name = map['name'].toString(); } diff --git a/packages/dart/lib/src/objects/parse_function.dart b/packages/dart/lib/src/objects/parse_function.dart index 8de148822..71afd0fff 100644 --- a/packages/dart/lib/src/objects/parse_function.dart +++ b/packages/dart/lib/src/objects/parse_function.dart @@ -33,8 +33,9 @@ class ParseCloudFunction extends ParseObject { _setObjectData(parameters); } - final Response result = await _client.post(uri, - headers: headers, body: json.encode(_getObjectData())); + final Response result = await _client.post(uri, + options: Options(headers: headers), + data: json.encode(_getObjectData())); return handleResponse( this, result, ParseApiRQ.execute, _debug, parseClassName); } @@ -48,8 +49,9 @@ class ParseCloudFunction extends ParseObject { if (parameters != null) { _setObjectData(parameters); } - final Response result = await _client.post(uri, - headers: headers, body: json.encode(_getObjectData())); + final Response result = await _client.post(uri, + options: Options(headers: headers), + data: json.encode(_getObjectData())); return handleResponse(this, result, ParseApiRQ.executeObjectionFunction, _debug, parseClassName); } diff --git a/packages/dart/lib/src/objects/parse_installation.dart b/packages/dart/lib/src/objects/parse_installation.dart index c30937742..344770f16 100644 --- a/packages/dart/lib/src/objects/parse_installation.dart +++ b/packages/dart/lib/src/objects/parse_installation.dart @@ -169,13 +169,13 @@ class ParseInstallation extends ParseObject { ParseApiRQ.create.toString(), uri, body); } - final Response result = - await _client.post(uri, body: body, headers: headers); + final Response result = await _client.post(uri, + data: body, options: Options(headers: headers)); //Set the objectId on the object after it is created. //This allows you to perform operations on the object after creation if (result.statusCode == 201) { - final Map map = json.decode(result.body); + final Map map = json.decode(result.data); objectId = map['objectId'].toString(); } @@ -199,7 +199,8 @@ class ParseInstallation extends ParseObject { logRequest(ParseCoreData().appName, parseClassName, ParseApiRQ.save.toString(), uri, body); } - final Response result = await _client.put(uri, body: body); + final Response result = + await _client.put(uri, data: body); return handleResponse( this, result, ParseApiRQ.save, _debug, parseClassName); } on Exception catch (e) { diff --git a/packages/dart/lib/src/objects/parse_object.dart b/packages/dart/lib/src/objects/parse_object.dart index 94d9a1576..35a9390c3 100644 --- a/packages/dart/lib/src/objects/parse_object.dart +++ b/packages/dart/lib/src/objects/parse_object.dart @@ -44,7 +44,7 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$uri'); - final Response result = await _client.get(url); + final Response result = await _client.get(url.toString()); return handleResponse( this, result, ParseApiRQ.get, _debug, parseClassName); } on Exception catch (e) { @@ -56,7 +56,7 @@ class ParseObject extends ParseBase implements ParseCloneable { Future getAll() async { try { final Uri url = getSanitisedUri(_client, '$_path'); - final Response result = await _client.get(url); + final Response result = await _client.get(url.toString()); return handleResponse( this, result, ParseApiRQ.getAll, _debug, parseClassName); } on Exception catch (e) { @@ -70,7 +70,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$_path'); final String body = json.encode(toJson(forApiRQ: true)); _saveChanges(); - final Response result = await _client.post(url, body: body); + final Response result = + await _client.post(url.toString(), data: body); return handleResponse( this, result, ParseApiRQ.create, _debug, parseClassName); @@ -87,8 +88,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Map headers = { keyHeaderContentType: keyHeaderContentTypeJson }; - final Response result = - await _client.put(url, body: body, headers: headers); + final Response result = await _client.put(url.toString(), + data: body, options: Options(headers: headers)); return handleResponse( this, result, ParseApiRQ.save, _debug, parseClassName); } on Exception catch (e) { @@ -410,7 +411,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); final String body = '{\"$key\":{\"__op\":\"$arrayAction\",\"objects\":${json.encode(parseEncode(values))}}}'; - final Response result = await _client.put(url, body: body); + final Response result = + await _client.put(url.toString(), data: body); return handleResponse( this, result, apiRQType, _debug, parseClassName); } else { @@ -468,7 +470,8 @@ class ParseObject extends ParseBase implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); final String body = '{\"$key\":{\"__op\":\"$countAction\",\"amount\":$amount}}'; - final Response result = await _client.put(url, body: body); + final Response result = + await _client.put(url.toString(), data: body); return handleResponse( this, result, apiRQType, _debug, parseClassName); } else { @@ -496,7 +499,8 @@ class ParseObject extends ParseBase implements ParseCloneable { if (objectId != null) { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); final String body = '{\"$key\":{\"__op\":\"Delete\"}}'; - final Response result = await _client.put(url, body: body); + final Response result = + await _client.put(url.toString(), data: body); final ParseResponse response = handleResponse( this, result, ParseApiRQ.unset, _debug, parseClassName); if (!response.success) { @@ -517,10 +521,14 @@ class ParseObject extends ParseBase implements ParseCloneable { } /// Can be used to create custom queries - Future query(String query) async { + Future query(String query, + {ProgressCallback progressCallback}) async { try { final Uri url = getSanitisedUri(_client, '$_path', query: query); - final Response result = await _client.get(url); + final Response result = await _client.get( + url.toString(), + onReceiveProgress: progressCallback, + ); return handleResponse( this, result, ParseApiRQ.query, _debug, parseClassName); } on Exception catch (e) { @@ -531,7 +539,7 @@ class ParseObject extends ParseBase implements ParseCloneable { Future distinct(String query) async { try { final Uri url = getSanitisedUri(_client, '$_aggregatepath', query: query); - final Response result = await _client.get(url); + final Response result = await _client.get(url.toString()); return handleResponse( this, result, ParseApiRQ.query, _debug, parseClassName); } on Exception catch (e) { @@ -546,7 +554,8 @@ class ParseObject extends ParseBase implements ParseCloneable { path ??= _path; id ??= objectId; final Uri url = getSanitisedUri(_client, '$_path/$id'); - final Response result = await _client.delete(url); + final Response result = + await _client.delete(url.toString()); return handleResponse( this, result, ParseApiRQ.delete, _debug, parseClassName); } on Exception catch (e) { diff --git a/packages/dart/lib/src/objects/parse_session.dart b/packages/dart/lib/src/objects/parse_session.dart index c02e1fe04..ab8f5105f 100644 --- a/packages/dart/lib/src/objects/parse_session.dart +++ b/packages/dart/lib/src/objects/parse_session.dart @@ -35,7 +35,8 @@ class ParseSession extends ParseObject implements ParseCloneable { const String path = '$keyEndPointSessions/me'; final Uri url = getSanitisedUri(_client, path); - final Response response = await _client.get(url); + final Response response = + await _client.get(url.toString()); return handleResponse( this, response, ParseApiRQ.logout, _debug, parseClassName); diff --git a/packages/dart/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart index 6df715954..719c94888 100644 --- a/packages/dart/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -115,7 +115,8 @@ class ParseUser extends ParseObject implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$keyEndPointUserName'); - final Response response = await _client.get(url, headers: headers); + final Response response = await _client + .get(url.toString(), options: Options(headers: headers)); return await _handleResponse( this, response, ParseApiRQ.currentUser, _debug, parseClassName); } on Exception catch (e) { @@ -154,12 +155,14 @@ class ParseUser extends ParseObject implements ParseCloneable { final String body = json.encode(bodyData); _saveChanges(); final String installationId = await _getInstallationId(); - final Response response = await _client.post(url, - headers: { - keyHeaderRevocableSession: '1', - if (installationId != null) keyHeaderInstallationId: installationId, - }, - body: body); + final Response response = + await _client.post(url.toString(), + options: Options(headers: { + keyHeaderRevocableSession: '1', + if (installationId != null) + keyHeaderInstallationId: installationId, + }), + data: body); return await _handleResponse( this, response, ParseApiRQ.signUp, _debug, parseClassName); @@ -184,11 +187,13 @@ class ParseUser extends ParseObject implements ParseCloneable { final Uri url = getSanitisedUri(_client, '$keyEndPointLogin', queryParams: queryParams); _saveChanges(); - final Response response = - await _client.get(url, headers: { - keyHeaderRevocableSession: '1', - if (installationId != null) keyHeaderInstallationId: installationId, - }); + final Response response = await _client.get( + url.toString(), + options: Options(headers: { + keyHeaderRevocableSession: '1', + if (installationId != null) keyHeaderInstallationId: installationId, + }), + ); return await _handleResponse( this, response, ParseApiRQ.login, _debug, parseClassName); @@ -205,16 +210,18 @@ class ParseUser extends ParseObject implements ParseCloneable { final Uuid uuid = Uuid(); final String installationId = await _getInstallationId(); - final Response response = await _client.post(url, - headers: { - keyHeaderRevocableSession: '1', - if (installationId != null) keyHeaderInstallationId: installationId, - }, - body: jsonEncode({ - 'authData': { - 'anonymous': {'id': uuid.v4()} - } - })); + final Response response = await _client.post( + url.toString(), + options: Options(headers: { + keyHeaderRevocableSession: '1', + if (installationId != null) keyHeaderInstallationId: installationId, + }), + data: jsonEncode({ + 'authData': { + 'anonymous': {'id': uuid.v4()} + } + }), + ); return await _handleResponse( this, response, ParseApiRQ.loginAnonymous, _debug, parseClassName); @@ -236,14 +243,16 @@ class ParseUser extends ParseObject implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$keyEndPointUsers'); final String installationId = await _getInstallationId(); - final Response response = await _client.post(url, - headers: { - keyHeaderRevocableSession: '1', - if (installationId != null) keyHeaderInstallationId: installationId, - }, - body: jsonEncode({ - 'authData': {provider: authData} - })); + final Response response = await _client.post( + url.toString(), + options: Options(headers: { + keyHeaderRevocableSession: '1', + if (installationId != null) keyHeaderInstallationId: installationId, + }), + data: jsonEncode({ + 'authData': {provider: authData} + }), + ); return await _handleResponse( this, response, ParseApiRQ.loginWith, _debug, parseClassName); @@ -266,8 +275,11 @@ class ParseUser extends ParseObject implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$keyEndPointLogout'); - final Response response = await _client.post(url, - headers: {keyHeaderSessionToken: sessionId}); + final Response response = await _client.post( + url.toString(), + options: Options( + headers: {keyHeaderSessionToken: sessionId}), + ); return await _handleResponse( this, response, ParseApiRQ.logout, _debug, parseClassName); @@ -290,9 +302,10 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Sends a verification email to the users email address Future verificationEmailRequest() async { try { - final Response response = await _client.post( - '${_client.data.serverUrl}$keyEndPointVerificationEmail', - body: json.encode({keyVarEmail: emailAddress})); + final Response response = await _client.post( + '${_client.data.serverUrl}$keyEndPointVerificationEmail', + data: json.encode({keyVarEmail: emailAddress}), + ); return await _handleResponse(this, response, ParseApiRQ.verificationEmailRequest, _debug, parseClassName); } on Exception catch (e) { @@ -304,9 +317,10 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Sends a password reset email to the users email address Future requestPasswordReset() async { try { - final Response response = await _client.post( - '${_client.data.serverUrl}$keyEndPointRequestPasswordReset', - body: json.encode({keyVarEmail: emailAddress})); + final Response response = await _client.post( + '${_client.data.serverUrl}$keyEndPointRequestPasswordReset', + data: json.encode({keyVarEmail: emailAddress}), + ); return await _handleResponse(this, response, ParseApiRQ.requestPasswordReset, _debug, parseClassName); } on Exception catch (e) { @@ -354,7 +368,8 @@ class ParseUser extends ParseObject implements ParseCloneable { if (objectId != null) { try { final Uri url = getSanitisedUri(_client, '$_path/$objectId'); - final Response response = await _client.delete(url); + final Response response = + await _client.delete(url.toString()); return await _handleResponse( this, response, ParseApiRQ.destroy, _debug, parseClassName); } on Exception catch (e) { @@ -377,7 +392,8 @@ class ParseUser extends ParseObject implements ParseCloneable { try { final Uri url = getSanitisedUri(_client, '$path'); - final Response response = await _client.get(url); + final Response response = + await _client.get(url.toString()); final ParseResponse parseResponse = handleResponse( emptyUser, response, ParseApiRQ.getAll, _debug, keyClassUser); return parseResponse; @@ -405,12 +421,16 @@ class ParseUser extends ParseObject implements ParseCloneable { } /// Handles all the response data for this class - static Future _handleResponse(ParseUser user, - Response response, ParseApiRQ type, bool debug, String className) async { + static Future _handleResponse( + ParseUser user, + Response response, + ParseApiRQ type, + bool debug, + String className) async { final ParseResponse parseResponse = handleResponse(user, response, type, debug, className); - final Map responseData = jsonDecode(response.body); + final Map responseData = jsonDecode(response.data); if (responseData.containsKey(keyVarObjectId)) { user.sessionToken = responseData[keyParamSessionToken]; ParseCoreData().setSessionId(user.sessionToken); diff --git a/packages/dart/lib/src/objects/response/parse_error_response.dart b/packages/dart/lib/src/objects/response/parse_error_response.dart index f7a50a623..e73360709 100644 --- a/packages/dart/lib/src/objects/response/parse_error_response.dart +++ b/packages/dart/lib/src/objects/response/parse_error_response.dart @@ -1,12 +1,13 @@ part of flutter_parse_sdk; /// Handles any errors returned in response -ParseResponse buildErrorResponse(ParseResponse response, Response apiResponse) { - if (apiResponse.body == null) { +ParseResponse buildErrorResponse( + ParseResponse response, Response apiResponse) { + if (apiResponse.data == null) { return null; } - final Map responseData = json.decode(apiResponse.body); + final Map responseData = json.decode(apiResponse.data); response.error = ParseError( code: responseData[keyCode], message: responseData[keyError].toString()); response.statusCode = responseData[keyCode]; diff --git a/packages/dart/lib/src/objects/response/parse_response_builder.dart b/packages/dart/lib/src/objects/response/parse_response_builder.dart index cf6ff8f14..41631ac43 100644 --- a/packages/dart/lib/src/objects/response/parse_response_builder.dart +++ b/packages/dart/lib/src/objects/response/parse_response_builder.dart @@ -9,7 +9,7 @@ part of flutter_parse_sdk; /// 4. Success with results. Again [ParseResponse()] is returned class _ParseResponseBuilder { ParseResponse handleResponse( - dynamic object, Response apiResponse, ParseApiRQ type) { + dynamic object, Response apiResponse, ParseApiRQ type) { final ParseResponse parseResponse = ParseResponse(); final bool returnAsResult = shouldReturnAsABaseResult(type); if (apiResponse != null) { @@ -25,9 +25,9 @@ class _ParseResponseBuilder { parseResponse, 1, 'Successful request, but no results found'); } else if (returnAsResult) { return _handleSuccessWithoutParseObject( - parseResponse, object, apiResponse.body); + parseResponse, object, apiResponse.data); } else { - return _handleSuccess(parseResponse, object, apiResponse.body, type); + return _handleSuccess(parseResponse, object, apiResponse.data, type); } } else { parseResponse.error = ParseError( @@ -150,7 +150,7 @@ class _ParseResponseBuilder { } } - bool isHealthCheck(Response apiResponse) { - return ['{\"status\":\"ok\"}', 'OK'].contains(apiResponse.body); + bool isHealthCheck(Response apiResponse) { + return ['{\"status\":\"ok\"}', 'OK'].contains(apiResponse.data); } } diff --git a/packages/dart/lib/src/objects/response/parse_response_utils.dart b/packages/dart/lib/src/objects/response/parse_response_utils.dart index 0201ee539..864e63654 100644 --- a/packages/dart/lib/src/objects/response/parse_response_utils.dart +++ b/packages/dart/lib/src/objects/response/parse_response_utils.dart @@ -2,10 +2,10 @@ part of flutter_parse_sdk; /// Handles an API response and logs data if [bool] debug is enabled @protected -ParseResponse handleResponse(dynamic object, Response response, +ParseResponse handleResponse(dynamic object, Response response, ParseApiRQ type, bool debug, String className) { final ParseResponse parseResponse = - _ParseResponseBuilder().handleResponse(object, response, type); + _ParseResponseBuilder().handleResponse(object, response, type); if (debug) { logAPIResponse(className, type.toString(), parseResponse); @@ -19,7 +19,7 @@ ParseResponse handleResponse(dynamic object, Response response, ParseResponse handleException( Exception exception, ParseApiRQ type, bool debug, String className) { final ParseResponse parseResponse = - buildParseResponseWithException(exception); + buildParseResponseWithException(exception); if (debug) { logAPIResponse(className, type.toString(), parseResponse); @@ -46,11 +46,11 @@ bool shouldReturnAsABaseResult(ParseApiRQ type) { } } -bool isUnsuccessfulResponse(Response apiResponse) => +bool isUnsuccessfulResponse(Response apiResponse) => apiResponse.statusCode != 200 && apiResponse.statusCode != 201; -bool isSuccessButNoResults(Response apiResponse) { - final dynamic decodedResponse = jsonDecode(apiResponse.body); +bool isSuccessButNoResults(Response apiResponse) { + final dynamic decodedResponse = jsonDecode(apiResponse.data); List results; if (decodedResponse is Map) { results = decodedResponse['results']; diff --git a/packages/dart/lib/src/utils/parse_file_extensions.dart b/packages/dart/lib/src/utils/parse_file_extensions.dart index 6482c2c41..d0454b691 100644 --- a/packages/dart/lib/src/utils/parse_file_extensions.dart +++ b/packages/dart/lib/src/utils/parse_file_extensions.dart @@ -3,6 +3,7 @@ part of flutter_parse_sdk; // ignore_for_file: always_specify_types /// Get the extension type of the file +@deprecated String getExtension(String contentType) { if (_extensions.containsKey(contentType) && _extensions[contentType].containsKey('extensions')) { @@ -12,6 +13,7 @@ String getExtension(String contentType) { } /// Get the content type based on +@deprecated String getContentType(String extension) { final Map extensions = _queryExtensions(); if (extension.lastIndexOf('.') >= 0) { @@ -26,6 +28,7 @@ String getContentType(String extension) { } /// Add content types based on extension to a map +@deprecated Map _queryExtensions() { Map extensions = Map(); @@ -43,6 +46,7 @@ Map _queryExtensions() { return extensions; } +@deprecated final Map _extensions = { 'application/1d-interleaved-parityfec': {'source': 'iana'}, 'application/3gpdash-qoe-report+xml': { diff --git a/packages/dart/lib/src/utils/parse_logger.dart b/packages/dart/lib/src/utils/parse_logger.dart index cb10ef8f5..2a0530d73 100644 --- a/packages/dart/lib/src/utils/parse_logger.dart +++ b/packages/dart/lib/src/utils/parse_logger.dart @@ -1,7 +1,7 @@ part of flutter_parse_sdk; -void logAPIResponse(String className, String type, - ParseResponse parseResponse) { +void logAPIResponse( + String className, String type, ParseResponse parseResponse) { const String spacer = ' \n'; String responseString = ''; @@ -31,26 +31,28 @@ void logAPIResponse(String className, String type, print(responseString); } -void logCUrl(BaseRequest request) { +void logCUrl(dio.Options options, dynamic data, String url) { String curlCmd = 'curl'; - curlCmd += ' -X ' + request.method; + curlCmd += ' -X ' + options.method; bool compressed = false; - request.headers.forEach((String name, String value) { + options.headers.forEach((String name, dynamic value) { if (name?.toLowerCase() == 'accept-encoding' && - value?.toLowerCase() == 'gzip') { + value?.toString()?.toLowerCase() == 'gzip') { compressed = true; } curlCmd += ' -H \'$name: $value\''; }); - if (request.method == 'POST' || request.method == 'PUT') { - if (request is Request) { - final String body = latin1.decode(request.bodyBytes); - curlCmd += ' -d \'$body\''; - } - } - curlCmd += (compressed ? ' --compressed ' : ' ') + request.url.toString(); - curlCmd += '\n\n ${Uri.decodeFull(request.url.toString())}'; + //TODO: log request + // if (options.method == 'POST' || options.method == 'PUT') { + // if (request is Request) { + // final String body = latin1.decode(request.bodyBytes); + // curlCmd += ' -d \'$body\''; + // } + // } + + curlCmd += (compressed ? ' --compressed ' : ' ') + url; + curlCmd += '\n\n ${Uri.decodeFull(url)}'; print('╭-- Parse Request'); print(curlCmd); print('╰--'); diff --git a/packages/dart/lib/src/utils/parse_utils.dart b/packages/dart/lib/src/utils/parse_utils.dart index e39ff5d03..543521b24 100644 --- a/packages/dart/lib/src/utils/parse_utils.dart +++ b/packages/dart/lib/src/utils/parse_utils.dart @@ -76,7 +76,8 @@ Future batchRequest( try { final Uri url = getSanitisedUri(client, '/batch'); final String body = json.encode({'requests': requests}); - final Response result = await client.post(url, body: body); + final Response result = + await client.post(url.toString(), data: body); return handleResponse( objects, result, ParseApiRQ.batch, debug, 'parse_utils'); diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml index da4f07621..1d75ec13a 100644 --- a/packages/dart/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -7,9 +7,9 @@ environment: sdk: ">=2.2.2 <3.0.0" dependencies: - http: ^0.12.2 # Networking + dio: ^3.0.10 web_socket_channel: ^1.1.0 #Database @@ -21,6 +21,7 @@ dependencies: uuid: ^2.2.2 meta: ^1.1.8 path: ^1.7.0 + mime_type: ^0.3.2 dev_dependencies: # Testing diff --git a/packages/dart/test/parse_query_test.dart b/packages/dart/test/parse_query_test.dart index 7a8ccb563..c95c373c8 100644 --- a/packages/dart/test/parse_query_test.dart +++ b/packages/dart/test/parse_query_test.dart @@ -30,7 +30,8 @@ void main() { when(client.data).thenReturn(ParseCoreData()); await queryBuilder.query(); - final Uri result = verify(client.get(captureAny)).captured.single; + final Uri result = + Uri.parse(verify(client.get(captureAny)).captured.single); expect(result.path, '/classes/_User'); diff --git a/packages/flutter/README.md b/packages/flutter/README.md index 7fd06cba4..c5ed1afa8 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -861,6 +861,7 @@ These classes are used by default to represent files, but you can also build you Have a look at the example application for a small (non web) example. +When uploading or downloading a file, you can use the `progressCallback`-parameter to track the progress of the http request. ```dart //A short example for showing an image from a ParseFileBase diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index fad78ea80..ce9fc83c8 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -7,7 +7,6 @@ environment: sdk: ">=2.2.2 <3.0.0" dependencies: - http: ^0.12.2 flutter: sdk: flutter @@ -15,6 +14,7 @@ dependencies: path: ../dart/ # Networking + dio: ^3.0.10 connectivity: ^0.4.9+2 # only used in the flutter part #Database diff --git a/packages/flutter/test/parse_query_test.dart b/packages/flutter/test/parse_query_test.dart index 678c11e80..7883c4b36 100644 --- a/packages/flutter/test/parse_query_test.dart +++ b/packages/flutter/test/parse_query_test.dart @@ -33,7 +33,8 @@ void main() { when(client.data).thenReturn(ParseCoreData()); await queryBuilder.query(); - final Uri result = verify(client.get(captureAny)).captured.single; + final Uri result = + Uri.parse(verify(client.get(captureAny)).captured.single); expect(result.path, '/classes/_User'); From 95c83b540baf2d64d5f0e6cd1666cbfab1a2475a Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 8 Oct 2020 18:11:46 +0200 Subject: [PATCH 21/33] improve readme (#468) --- README.md | 6 ++-- packages/dart/README.md | 57 +++++++++++++++++++++----------------- packages/flutter/README.md | 12 +++++--- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 54a5ffd26..3de7ab640 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ - -![Parse Logo](https://parseplatform.org/img/logo.svg) + --- diff --git a/packages/dart/README.md b/packages/dart/README.md index 625b4690f..e79a8f4cb 100644 --- a/packages/dart/README.md +++ b/packages/dart/README.md @@ -1,9 +1,16 @@ -![Parse Logo](https://parseplatform.org/img/logo.svg) ![Dart Logo](https://dart.dev/assets/shared/dart-logo-for-shares.png?2) +

+ Parse Logo + Dart Logo +

+--- -**THIS README IS WORK IN PROGRESS** +**This is now a dart package. The Flutter package was moved [here](https://pub.dev/packages/parse_server_sdk_flutter).** -## Parse For Dart! +**THIS README IS WORK IN PROGRESS!** +Help out to improve this README on [GitHub](https://github.com/parse-community/Parse-SDK-Flutter/edit/master/packages/dart/README.md). + +## Parse For Dart! This is a Dart package that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). This is a work in progress and we are consistently updating it. Please let us know if you think anything needs changing/adding, and more than ever, please do join in on this project. (Even if it is just to improve our documentation) @@ -36,7 +43,7 @@ await Parse().initialize( keyParseServerUrl, coreStore: await CoreStoreSembastImp.getInstance("/data")); ``` -It's possible to add other parameters to work with your instance of Parse Server:- +It's possible to add other parameters to work with your instance of Parse Server:- ```dart await Parse().initialize( @@ -307,7 +314,7 @@ The features available are:- * Ascending * Descending * Plenty more! - + ## Relational queries If you want to retrieve objects where a field contains an object that matches another query, you can use the __whereMatchesQuery__ condition. @@ -429,7 +436,7 @@ QueryBuilder query = ..whereEqualTo('intNumber', 1); ``` __Create a subscription__ -You’ll get the LiveQuery events through this subscription. +You’ll get the LiveQuery events through this subscription. The first time you call subscribe, we’ll try to open the WebSocket connection to the LiveQuery server for you. ```dart @@ -440,7 +447,7 @@ __Event Handling__ We define several types of events you’ll get through a subscription object: __Create event__ -When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. +When a new ParseObject is created and it fulfills the QueryBuilder you subscribe, you’ll get this event. The object is the ParseObject which was created. ```dart subscription.on(LiveQueryEvent.create, (value) { @@ -455,8 +462,8 @@ subscription.on(LiveQueryEvent.create, (value) { ``` __Update event__ -When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the -QueryBuilder before and after changes), you’ll get this event. +When an existing ParseObject which fulfills the QueryBuilder you subscribe is updated (The ParseObject fulfills the +QueryBuilder before and after changes), you’ll get this event. The object is the ParseObject which was updated. Its content is the latest value of the ParseObject. ```dart subscription.on(LiveQueryEvent.update, (value) { @@ -471,8 +478,8 @@ subscription.on(LiveQueryEvent.update, (value) { ``` __Enter event__ -When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, -you’ll get this event. The object is the ParseObject which enters the QueryBuilder. +When an existing ParseObject’s old value does not fulfill the QueryBuilder but its new value fulfills the QueryBuilder, +you’ll get this event. The object is the ParseObject which enters the QueryBuilder. Its content is the latest value of the ParseObject. ```dart subscription.on(LiveQueryEvent.enter, (value) { @@ -487,8 +494,8 @@ subscription.on(LiveQueryEvent.enter, (value) { ``` __Leave event__ -When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, -you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. +When an existing ParseObject’s old value fulfills the QueryBuilder but its new value doesn’t fulfill the QueryBuilder, +you’ll get this event. The object is the ParseObject which leaves the QueryBuilder. Its content is the latest value of the ParseObject. ```dart subscription.on(LiveQueryEvent.leave, (value) { @@ -503,7 +510,7 @@ subscription.on(LiveQueryEvent.leave, (value) { ``` __Delete event__ -When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. +When an existing ParseObject which fulfills the QueryBuilder is deleted, you’ll get this event. The object is the ParseObject which is deleted ```dart subscription.on(LiveQueryEvent.delete, (value) { @@ -518,8 +525,8 @@ subscription.on(LiveQueryEvent.delete, (value) { ``` __Unsubscribe__ -If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. -After that, you won’t get any events from the subscription object and will close the WebSocket connection to the +If you would like to stop receiving events from a QueryBuilder, you can just unsubscribe the subscription. +After that, you won’t get any events from the subscription object and will close the WebSocket connection to the LiveQuery server. ```dart @@ -594,13 +601,13 @@ Other user features are:- * Get all users * Save * Destroy user - * Queries - + * Queries + ## Facebook, OAuth and 3rd Party Login/User - + Usually, each provider will provide their own library for logins, but the loginWith method on ParseUser accepts a name of provider, then a Map with the authentication details required. For Facebook and the example below, we used the library provided at https://pub.dev/packages/flutter_facebook_login - + ``` Future goToFacebookLogin() async { final FacebookLogin facebookLogin = FacebookLogin(); @@ -651,9 +658,9 @@ For any object, you can specify which users are allowed to read the object, and To support this type of security, each object has an access control list, implemented by the __ParseACL__ class. If ParseACL is not specified (with the exception of the ParseUser class) all objects are set to Public for read and write. -The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. +The simplest way to use a ParseACL is to specify that an object may only be read or written by a single user. To create such an object, there must first be a logged in ParseUser. Then, new ParseACL(user) generates a ParseACL that -limits access to that user. An object’s ACL is updated when the object is saved, like any other property. +limits access to that user. An object’s ACL is updated when the object is saved, like any other property. ```dart ParseUser user = await ParseUser.currentUser() as ParseUser; @@ -664,7 +671,7 @@ ParseObject parseObject = ParseObject("TestAPI"); parseObject.setACL(parseACL); var apiResponse = await parseObject.save(); ``` -Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using +Permissions can also be granted on a per-user basis. You can add permissions individually to a ParseACL using __setReadAccess__ and __setWriteAccess__ ```dart ParseUser user = await ParseUser.currentUser() as ParseUser; @@ -693,8 +700,8 @@ parseObject.setACL(parseACL); var apiResponse = await parseObject.save(); ``` Operations that are forbidden, such as deleting an object that you do not have write access to, result in a -ParseError with code 101: 'ObjectNotFound'. -For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which +ParseError with code 101: 'ObjectNotFound'. +For security purposes, this prevents clients from distinguishing which object ids exist but are secured, versus which object ids do not exist at all. You can retrieve the ACL list of an object using: diff --git a/packages/flutter/README.md b/packages/flutter/README.md index c5ed1afa8..964e89ce4 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -1,5 +1,9 @@ +

+ Parse Logo + Flutter Logo +

-![Parse Logo](https://parseplatform.org/img/logo.svg) ![Flutter Logo](https://upload.wikimedia.org/wikipedia/commons/1/17/Google-flutter-logo.png) +--- ## Parse For Flutter! Hi, this is a Flutter plugin that allows communication with a Parse Server, (https://parseplatform.org) either hosted on your own server or another, like (http://Back4App.com). @@ -49,12 +53,12 @@ It's possible to add other parameters to work with your instance of Parse Server ``` -#### Early Web support -Currently this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. +#### Web support +Due to Cross-origin resource sharing (CORS) restrictions, this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. When running directly via docker, set the env var `PARSE_SERVER_ALLOW_HEADERS=X-Parse-Installation-Id`. When running via express, set [ParseServerOptions](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `allowHeaders: ['X-Parse-Installation-Id']`. -Be aware that for web ParseInstallation does include app name, version or package identifier. +Be aware that for web ParseInstallation does include app name, version or package identifier automatically. You should manually provide this data as described [here](https://github.com/parse-community/Parse-SDK-Flutter/blob/master/docs/migrate-1-0-28.md#optional-provide-app-information-on-web); ## Objects From 583bfb8d46cf3217f438fd326ee5b14be1b21ab5 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 8 Oct 2020 18:12:01 +0200 Subject: [PATCH 22/33] Update CHANGELOG.md (#462) --- packages/dart/CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/dart/CHANGELOG.md b/packages/dart/CHANGELOG.md index 6d36111d2..a5803d9ef 100644 --- a/packages/dart/CHANGELOG.md +++ b/packages/dart/CHANGELOG.md @@ -1,5 +1,11 @@ ## 1.0.28 -Spit this package: All flutter parts are now in [this](https://pub.dev/packages/parse_server_sdk_flutter) package! +- Added full web support +- split this package in a dart and a flutter one + - [flutter package](https://pub.dev/packages/parse_server_sdk_flutter) + - [dart package](https://pub.dev/packages/parse_server_sdk) +- Moved ParseHTTPClient to Dio [#459](https://github.com/parse-community/Parse-SDK-Flutter/pull/459) +- Bug fixes +- general improvements ## 1.0.27 User login / signUp / loginAnonymous delete SessionId stored in device before calling server From e8e1bb6da60a144ab666b20d6179cc17cf7f3f51 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Thu, 8 Oct 2020 20:53:27 +0200 Subject: [PATCH 23/33] allow signup without email (#469) --- packages/dart/lib/src/objects/parse_user.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/dart/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart index 719c94888..da1e3577f 100644 --- a/packages/dart/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -141,12 +141,20 @@ class ParseUser extends ParseObject implements ParseCloneable { /// /// After creating a new user via [Parse.create] call this method to register /// that user on Parse - Future signUp() async { + /// By setting [allowWithoutEmail] to `true`, you can sign up without setting an email + Future signUp({bool allowWithoutEmail = false}) async { forgetLocalSession(); try { if (emailAddress == null) { - return null; + if (!allowWithoutEmail) { + return null; + } else { + assert(() { + print('It is recommended to only allow user signUp with a '); + return true; + }()); + } } final Map bodyData = _getObjectData(); From ee68a84adbf0b1df558fcc7d4e4c8b659f930666 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 9 Oct 2020 13:19:03 +0200 Subject: [PATCH 24/33] Update migrate-1-0-28.md (#471) Added section for tests at migration guide. Fix: https://github.com/parse-community/Parse-SDK-Flutter/issues/385#issuecomment-695939265 --- docs/migrate-1-0-28.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/migrate-1-0-28.md b/docs/migrate-1-0-28.md index b6ae4fda4..e7771ad91 100644 --- a/docs/migrate-1-0-28.md +++ b/docs/migrate-1-0-28.md @@ -43,6 +43,10 @@ Parse().initialize( ); ``` +### optional: adjust your tests +You tests might fail if the package tries to automatically discover facts like you applications name, version, package name or the default file directory. +You can avoid this automatic discovery by providing values for `appName`, `appVersion`, `appPackageName` as well as `fileDirectory` at you `Parse().initialize()` call. + ### changed network library In order to provide a `ProgressCallback` for heavy file operations, the network library was switched from [http](https://pub.dev/packages/http) to [dio](https://pub.dev/packages/dio). From 65b5a4884aa711324c08a6157d3aea2d37537011 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Fri, 9 Oct 2020 16:35:36 +0200 Subject: [PATCH 25/33] SignUp encoding data to JSON (#470) * signup - use toJson Co-Authored-By: itmesh <50654062+itmesh@users.noreply.github.com> * fix password update --- packages/dart/lib/src/objects/parse_user.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/dart/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart index da1e3577f..6429d3c3d 100644 --- a/packages/dart/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -11,7 +11,7 @@ class ParseUser extends ParseObject implements ParseCloneable { /// Requires [String] username, [String] password. [String] email address /// is required as well to create a full new user object on ParseServer. Only /// username and password is required to login - ParseUser(String username, this.password, String emailAddress, + ParseUser(String username, String password, String emailAddress, {String sessionToken, bool debug, ParseHTTPClient client}) : super(keyClassUser) { _debug = isDebugEnabled(objectLevelDebug: debug); @@ -22,6 +22,7 @@ class ParseUser extends ParseObject implements ParseCloneable { this.username = username; this.emailAddress = emailAddress; + this.password = password; this.sessionToken = sessionToken; } @@ -39,7 +40,16 @@ class ParseUser extends ParseObject implements ParseCloneable { static const String keyEmailAddress = 'email'; static const String path = '$keyEndPointClasses$keyClassUser'; - String password; + String _password; + + String get password => _password; + + set password(String password) { + if (_password != password) { + _password = password; + if (password != null) _unsavedChanges[keyVarPassword] = password; + } + } Map get acl => super.get>(keyVarAcl); @@ -157,10 +167,8 @@ class ParseUser extends ParseObject implements ParseCloneable { } } - final Map bodyData = _getObjectData(); - bodyData[keyVarPassword] = password; final Uri url = getSanitisedUri(_client, '$path'); - final String body = json.encode(bodyData); + final String body = json.encode(toJson(forApiRQ: true)); _saveChanges(); final String installationId = await _getInstallationId(); final Response response = From ceb17f63d6276aec45bd7625fbf086397c249bd6 Mon Sep 17 00:00:00 2001 From: Nils Strelow <1096841+nstrelow@users.noreply.github.com> Date: Fri, 9 Oct 2020 20:52:46 +0200 Subject: [PATCH 26/33] Add disclaimer to not use Masterkey on client side (#474) * Add disclaimer to not use Masterkey on client side --- packages/dart/README.md | 2 +- packages/flutter/README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dart/README.md b/packages/dart/README.md index e79a8f4cb..b57447dbc 100644 --- a/packages/dart/README.md +++ b/packages/dart/README.md @@ -57,7 +57,7 @@ It's possible to add other parameters to work with your instance of Parse Server securityContext: securityContext, // Again, required for some setups coreStore: CoreStoreMemoryImp()); // Non persistent mode (default): Sdk will store everything in memmore instead of using Sembast as an internal DB. ``` - +⚠️ Please note that the master key should only be used in safe environments and never on client side ‼️ Using this package on a server should be fine. #### Early Web support Currently this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. diff --git a/packages/flutter/README.md b/packages/flutter/README.md index 964e89ce4..5ee3de12e 100644 --- a/packages/flutter/README.md +++ b/packages/flutter/README.md @@ -52,6 +52,7 @@ It's possible to add other parameters to work with your instance of Parse Server coreStore: await CoreStoreSharedPrefsImp.getInstance()); // Local data storage method. Will use SharedPreferences instead of Sembast as an internal DB ``` +⚠️ Please note that the master key should only be used in safe environments and never on client side ‼️ #### Web support Due to Cross-origin resource sharing (CORS) restrictions, this requires adding `X-Parse-Installation-Id` as an allowed header to parse-server. From 4622e29a4c231e68ba9b218d2b170db8814ff1d2 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sat, 10 Oct 2020 01:37:18 +0200 Subject: [PATCH 27/33] LiveList: fix setState after dispose (#475) --- .../flutter/lib/src/utils/parse_live_list.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flutter/lib/src/utils/parse_live_list.dart b/packages/flutter/lib/src/utils/parse_live_list.dart index d80efed77..0fca66328 100644 --- a/packages/flutter/lib/src/utils/parse_live_list.dart +++ b/packages/flutter/lib/src/utils/parse_live_list.dart @@ -134,6 +134,13 @@ class _ParseLiveListWidgetState : buildAnimatedList(); } + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + Widget buildAnimatedList() { return AnimatedList( key: _animatedListKey, @@ -235,6 +242,13 @@ class _ParseLiveListElementWidgetState StreamSubscription _streamSubscription; + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + @override void dispose() { _streamSubscription?.cancel(); From eee69fc7d9402b825e93b119cc70be8572ea4f34 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sat, 10 Oct 2020 20:38:14 +0200 Subject: [PATCH 28/33] Querybuilder: whereMatchesKeyInQuery bugfix (#476) Fix bug where whereMatchesKeyInQuery cant be used when using different object classes. --- packages/dart/lib/src/network/parse_query.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dart/lib/src/network/parse_query.dart b/packages/dart/lib/src/network/parse_query.dart index 497fe928b..7f995427a 100644 --- a/packages/dart/lib/src/network/parse_query.dart +++ b/packages/dart/lib/src/network/parse_query.dart @@ -301,7 +301,7 @@ class QueryBuilder { // Add a constraint to the query that requires a particular key's value matches a value for a key in the results of another ParseQuery. // ignore: always_specify_types void whereMatchesKeyInQuery( - String column, String keyInQuery, QueryBuilder query) { + String column, String keyInQuery, QueryBuilder query) { if (query.queries.isEmpty) { throw ArgumentError('query conditions is required'); } @@ -322,7 +322,7 @@ class QueryBuilder { // Add a constraint to the query that requires a particular key's value does not match any value for a key in the results of another ParseQuery // ignore: always_specify_types void whereDoesNotMatchKeyInQuery( - String column, String keyInQuery, QueryBuilder query) { + String column, String keyInQuery, QueryBuilder query) { if (query.queries.isEmpty) { throw ArgumentError('query conditions is required'); } From b0dd5b8e0616485d455965430af0138963969c4c Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sat, 10 Oct 2020 22:51:51 +0200 Subject: [PATCH 29/33] fixed liveListRetryIntervals in flutter Not sure how this line got lost. --- packages/flutter/lib/parse_server_sdk.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter/lib/parse_server_sdk.dart b/packages/flutter/lib/parse_server_sdk.dart index a0b26e1ee..92950b138 100644 --- a/packages/flutter/lib/parse_server_sdk.dart +++ b/packages/flutter/lib/parse_server_sdk.dart @@ -90,6 +90,7 @@ class Parse extends sdk.Parse registeredSubClassMap: registeredSubClassMap, parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, + liveListRetryIntervals: liveListRetryIntervals, connectivityProvider: connectivityProvider ?? this, fileDirectory: fileDirectory ?? (!sdk.parseIsWeb ? (await getTemporaryDirectory()).path : null), From 4d43411ce925e10d8d1d2a88555641b26090401f Mon Sep 17 00:00:00 2001 From: wigginsp Date: Sun, 11 Oct 2020 09:54:58 +0100 Subject: [PATCH 30/33] Rel 1.0.28: Add license to sub-projects --- packages/dart/LICENSE | 31 +++++++++++++++++++++++++++++++ packages/flutter/LICENSE | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/dart/LICENSE create mode 100644 packages/flutter/LICENSE diff --git a/packages/dart/LICENSE b/packages/dart/LICENSE new file mode 100644 index 000000000..38f25d526 --- /dev/null +++ b/packages/dart/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2019, Phill Wiggins. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======= diff --git a/packages/flutter/LICENSE b/packages/flutter/LICENSE new file mode 100644 index 000000000..38f25d526 --- /dev/null +++ b/packages/flutter/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2019, Phill Wiggins. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======= From e648f0a762ba0e7555be0a51c4ad3e458ea2de6e Mon Sep 17 00:00:00 2001 From: wigginsp Date: Sun, 11 Oct 2020 10:09:16 +0100 Subject: [PATCH 31/33] Rel 1.0.28: Add dependencies required for rel --- packages/flutter/pubspec.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index ce9fc83c8..a89c69302 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -10,8 +10,7 @@ dependencies: flutter: sdk: flutter - parse_server_sdk: - path: ../dart/ + parse_server_sdk: ^1.0.28 # Networking dio: ^3.0.10 @@ -24,6 +23,9 @@ dependencies: path_provider: ^1.6.14 # only used in the flutter part package_info: ^0.4.3 # only used in the flutter part + sembast: ^2.4.7+6 # required for transitive use only + path: ^1.7.0 # required for transitive use only + dev_dependencies: # Testing flutter_test: From 9d0d8885b3f6996c1a56846570d498e0ecc31f5d Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sun, 11 Oct 2020 11:47:45 +0200 Subject: [PATCH 32/33] 1.0.28/fix publish warnings (#472) * added licenses I've just copied the LICENSE file. * change dependencies at dart/pubspec.yaml * change dependencies at flutter/pubspec.yaml --- packages/dart/pubspec.yaml | 2 +- packages/flutter/pubspec.yaml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dart/pubspec.yaml b/packages/dart/pubspec.yaml index 1d75ec13a..d78d35b1a 100644 --- a/packages/dart/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: #Database sembast: ^2.4.7+6 - sembast_web: '>=1.0.0' + sembast_web: ^1.2.0 xxtea: ^2.0.3 # Utils diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index a89c69302..d2fb769da 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: parse_server_sdk: ^1.0.28 # Networking - dio: ^3.0.10 connectivity: ^0.4.9+2 # only used in the flutter part #Database @@ -22,7 +21,6 @@ dependencies: # Utils path_provider: ^1.6.14 # only used in the flutter part package_info: ^0.4.3 # only used in the flutter part - sembast: ^2.4.7+6 # required for transitive use only path: ^1.7.0 # required for transitive use only From 9eca393a974bda0e33ad2d3d27cc133086bd36e7 Mon Sep 17 00:00:00 2001 From: Maximilian Fischer Date: Sun, 11 Oct 2020 11:51:12 +0200 Subject: [PATCH 33/33] flutter: added dio to pubspec.yaml It's contained in the pub.spec archive, so I add it here. --- packages/flutter/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index d2fb769da..16bfd4ee2 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: parse_server_sdk: ^1.0.28 # Networking + dio: ^3.0.10 connectivity: ^0.4.9+2 # only used in the flutter part #Database

+ Parse Logo + +