diff --git a/.travis.yml b/.travis.yml index 13c0c03b0..e1599d287 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,14 @@ 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: - - flutter packages get - - flutter test --no-pub 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/) cache: directories: diff --git a/README.md b/README.md index e30959fd3..3de7ab640 100644 --- a/README.md +++ b/README.md @@ -1,891 +1,24 @@ +

+ Parse Logo + +

-![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 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.27 -``` -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/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 | -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. -```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/docs/migrate-1-0-28.md b/docs/migrate-1-0-28.md new file mode 100644 index 000000000..e7771ad91 --- /dev/null +++ b/docs/migrate-1-0-28.md @@ -0,0 +1,53 @@ +# 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, +); +``` + +### 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). +There should be no breaking changes regarding this change, except if you are overriding the `ParseHTTPClient`. \ No newline at end of file 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 13372aef5..000000000 Binary files a/example/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ 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 "$@" diff --git a/lib/src/network/parse_http_client.dart b/lib/src/network/parse_http_client.dart deleted file mode 100644 index b577f4849..000000000 --- a/lib/src/network/parse_http_client.dart +++ /dev/null @@ -1,46 +0,0 @@ -part of flutter_parse_sdk; - -/// Creates a custom version of HTTP Client that has Parse Data Preset -class ParseHTTPClient extends BaseClient { - ParseHTTPClient({bool sendSessionId = false, SecurityContext securityContext}) - : _sendSessionId = sendSessionId, - _client = securityContext != null - ? IOClient(HttpClient(context: securityContext)) - : Client(); - - final Client _client; - final bool _sendSessionId; - final String _userAgent = '$keyLibraryName $keySdkVersion'; - ParseCoreData data = ParseCoreData(); - Map additionalHeaders; - - /// Overrides the call method for HTTP Client and adds custom headers - @override - Future send(BaseRequest request) { - if (!identical(0, 0.0)) { - request.headers[keyHeaderUserAgent] = _userAgent; - } - request.headers[keyHeaderApplicationId] = data.applicationId; - if ((_sendSessionId == true) && - (data.sessionId != null) && - (request.headers[keyHeaderSessionToken] == null)) - request.headers[keyHeaderSessionToken] = data.sessionId; - - if (data.clientKey != null) - request.headers[keyHeaderClientKey] = data.clientKey; - if (data.masterKey != null) - request.headers[keyHeaderMasterKey] = 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); - } - - if (data.debug) { - logCUrl(request); - } - - return _client.send(request); - } -} 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/storage/core_store.dart b/lib/src/storage/core_store.dart deleted file mode 100644 index b7d2fe450..000000000 --- a/lib/src/storage/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/CHANGELOG.md b/packages/dart/CHANGELOG.md similarity index 84% rename from CHANGELOG.md rename to packages/dart/CHANGELOG.md index 14398f151..a5803d9ef 100644 --- a/CHANGELOG.md +++ b/packages/dart/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.28 +- 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 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/dart/README.md b/packages/dart/README.md new file mode 100644 index 000000000..b57447dbc --- /dev/null +++ b/packages/dart/README.md @@ -0,0 +1,853 @@ +

+ Parse Logo + Dart Logo +

+ +--- + +**This is now a dart package. The Flutter package was moved [here](https://pub.dev/packages/parse_server_sdk_flutter).** + +**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) + +## 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("/data")); +``` +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: 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. +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. + +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){ + 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(); +``` +```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) +* 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 b/packages/dart/lib/parse_server_sdk.dart similarity index 67% rename from lib/parse_server_sdk.dart rename to packages/dart/lib/parse_server_sdk.dart index 606fd131c..7681b39ab 100644 --- a/lib/parse_server_sdk.dart +++ b/packages/dart/lib/parse_server_sdk.dart @@ -5,39 +5,30 @@ import 'dart:convert'; import 'dart:io'; 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:dio/dio.dart' hide Options; +import 'package:dio/dio.dart' as dio show Options; import 'package:meta/meta.dart'; -import 'package:package_info/package_info.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; -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:sembast_web/sembast_web.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'; -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/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'; part 'src/network/parse_query.dart'; part 'src/objects/parse_acl.dart'; part 'src/objects/parse_base.dart'; @@ -56,10 +47,20 @@ 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'; @@ -74,17 +75,20 @@ class Parse { /// /// ``` /// 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); + /// ``` 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, @@ -96,6 +100,9 @@ class Parse { ParseUserConstructor parseUserConstructor, ParseFileConstructor parseFileConstructor, List liveListRetryIntervals, + ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, }) async { final String url = removeTrailingSlash(serverUrl); @@ -104,6 +111,9 @@ class Parse { url, debug: debug, appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale, liveQueryUrl: liveQueryUrl, masterKey: masterKey, clientKey: clientKey, @@ -115,6 +125,9 @@ class Parse { parseUserConstructor: parseUserConstructor, parseFileConstructor: parseFileConstructor, liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider, + fileDirectory: fileDirectory, + appResumedStream: appResumedStream, ); _hasBeenInitialized = true; @@ -140,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/lib/src/base/parse_constants.dart b/packages/dart/lib/src/base/parse_constants.dart similarity index 96% rename from lib/src/base/parse_constants.dart rename to packages/dart/lib/src/base/parse_constants.dart index 56a3d9962..25fe69010 100644 --- a/lib/src/base/parse_constants.dart +++ b/packages/dart/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 @@ -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/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/parse_core_data.dart b/packages/dart/lib/src/data/parse_core_data.dart similarity index 80% rename from lib/src/data/parse_core_data.dart rename to packages/dart/lib/src/data/parse_core_data.dart index 02b37c2af..de4fe75c3 100644 --- a/lib/src/data/parse_core_data.dart +++ b/packages/dart/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,13 @@ 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); + _instance.storage ??= store ?? CoreStoreMemoryImp(); if (debug != null) { _instance.debug = debug; @@ -42,6 +47,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 +87,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 +116,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/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/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_connectivity.dart b/packages/dart/lib/src/network/parse_connectivity.dart new file mode 100644 index 000000000..731ad1a4b --- /dev/null +++ b/packages/dart/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/packages/dart/lib/src/network/parse_http_client.dart b/packages/dart/lib/src/network/parse_http_client.dart new file mode 100644 index 000000000..29c293477 --- /dev/null +++ b/packages/dart/lib/src/network/parse_http_client.dart @@ -0,0 +1,62 @@ +part of flutter_parse_sdk; + +/// Creates a custom version of HTTP Client that has Parse Data Preset +class ParseHTTPClient with DioMixin implements Dio { + ParseHTTPClient({bool sendSessionId = false, SecurityContext securityContext}) + : _sendSessionId = sendSessionId { + options = BaseOptions(); + httpClientAdapter = createHttpClientAdapter(securityContext); + } + + final bool _sendSessionId; + final String _userAgent = '$keyLibraryName $keySdkVersion'; + ParseCoreData data = ParseCoreData(); + Map additionalHeaders; + + /// Overrides the call method for HTTP Client and adds custom headers + @override + Future> request( + String path, { + dynamic data, + Map queryParameters, + CancelToken cancelToken, + dio.Options options, + ProgressCallback onSendProgress, + ProgressCallback onReceiveProgress, + }) { + options ??= Options(); + if (!identical(0, 0.0)) { + options.headers[keyHeaderUserAgent] = _userAgent; + } + options.headers[keyHeaderApplicationId] = this.data.applicationId; + if ((_sendSessionId == true) && + (this.data.sessionId != null) && + (options.headers[keyHeaderSessionToken] == null)) + options.headers[keyHeaderSessionToken] = this.data.sessionId; + + 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) => options.headers[key] = value); + } + + if (this.data.debug) { + logCUrl(options, data, path); + } + + return super.request( + path, + data: data, + queryParameters: queryParameters, + cancelToken: cancelToken, + options: options, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } +} diff --git a/lib/src/network/parse_live_query.dart b/packages/dart/lib/src/network/parse_live_query.dart similarity index 86% rename from lib/src/network/parse_live_query.dart rename to packages/dart/lib/src/network/parse_live_query.dart index ca90110c0..a1f151ecb 100644 --- a/lib/src/network/parse_live_query.dart +++ b/packages/dart/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; } @@ -43,12 +33,21 @@ class Subscription { enum LiveQueryClientEvent { CONNECTED, DISCONNECTED, USER_DISCONNECTED } -class LiveQueryReconnectingController with WidgetsBindingObserver { +class LiveQueryReconnectingController { LiveQueryReconnectingController( - this._reconnect, this._eventStream, this.debug) { - 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 { + print( + 'LiveQuery does not work, if there is ParseConnectivityProvider provided.'); + } _eventStream.listen((LiveQueryClientEvent event) { switch (event) { case LiveQueryClientEvent.CONNECTED: @@ -73,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; @@ -90,28 +89,20 @@ 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(state == ParseConnectivityResult.none) { + _isConnected = false; + } 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 && @@ -132,10 +123,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 +150,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 +163,7 @@ class Client { return _clientEventStream; } - WebSocket _webSocket; + parse_web_socket.WebSocket _webSocket; ParseHTTPClient _client; bool _debug; bool _sendSessionId; @@ -186,7 +174,6 @@ class Client { Stream _clientEventStream; LiveQueryReconnectingController reconnectingController; - // ignore: always_specify_types final Map _requestSubScription = {}; Future reconnect({bool userInitialized = false}) async { @@ -198,11 +185,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 +204,6 @@ class Client { await _channel.sink.close(); _channel = null; } - // ignore: always_specify_types _requestSubScription.values.toList().forEach((Subscription subscription) { subscription._enabled = false; }); @@ -274,9 +261,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 +274,7 @@ class Client { } return Future.value(null); } - _channel = IOWebSocketChannel(_webSocket); + _channel = _webSocket.createWebSocketChannel(); _channel.stream.listen((dynamic message) { _handleMessage(message); }, onDone: () { @@ -302,8 +290,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 +332,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 +375,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 +425,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_query.dart b/packages/dart/lib/src/network/parse_query.dart similarity index 98% rename from lib/src/network/parse_query.dart rename to packages/dart/lib/src/network/parse_query.dart index 9aca4aa3e..7f995427a 100644 --- a/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'); } @@ -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/network/parse_websocket.dart b/packages/dart/lib/src/network/parse_websocket.dart new file mode 100644 index 000000000..4ec07c900 --- /dev/null +++ b/packages/dart/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/packages/dart/lib/src/network/parse_websocket_html.dart b/packages/dart/lib/src/network/parse_websocket_html.dart new file mode 100644 index 000000000..aeab1d27c --- /dev/null +++ b/packages/dart/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/packages/dart/lib/src/network/parse_websocket_io.dart b/packages/dart/lib/src/network/parse_websocket_io.dart new file mode 100644 index 000000000..37206dd49 --- /dev/null +++ b/packages/dart/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); + } +} 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 90% rename from lib/src/objects/parse_config.dart rename to packages/dart/lib/src/objects/parse_config.dart index 8f7dedf16..1c2feb28b 100644 --- a/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/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 65% rename from lib/src/objects/parse_file.dart rename to packages/dart/lib/src/objects/parse_file.dart index c44d63ab3..76349ec41 100644 --- a/lib/src/objects/parse_file.dart +++ b/packages/dart/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(); @@ -42,23 +40,26 @@ class ParseFile extends ParseFileBase { } @override - Future download() async { + Future download({ProgressCallback progressCallback}) async { if (url == null) { 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); + 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 = { @@ -67,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/lib/src/objects/parse_file_base.dart b/packages/dart/lib/src/objects/parse_file_base.dart similarity index 89% rename from lib/src/objects/parse_file_base.dart rename to packages/dart/lib/src/objects/parse_file_base.dart index e565b92a7..4ca208259 100644 --- a/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/lib/src/objects/parse_file_web.dart b/packages/dart/lib/src/objects/parse_file_web.dart similarity index 61% rename from lib/src/objects/parse_file_web.dart rename to packages/dart/lib/src/objects/parse_file_web.dart index f17369a96..323fff54d 100644 --- a/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/lib/src/objects/parse_function.dart b/packages/dart/lib/src/objects/parse_function.dart similarity index 84% rename from lib/src/objects/parse_function.dart rename to packages/dart/lib/src/objects/parse_function.dart index 8de148822..71afd0fff 100644 --- a/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/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 91% rename from lib/src/objects/parse_installation.dart rename to packages/dart/lib/src/objects/parse_installation.dart index 34e9eaa45..344770f16 100644 --- a/lib/src/objects/parse_installation.dart +++ b/packages/dart/lib/src/objects/parse_installation.dart @@ -83,22 +83,14 @@ class ParseInstallation extends ParseObject { } //Locale - final String locale = parseIsWeb - ? ui.window.locale.toString() - : await Devicelocale.currentLocale; - 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); } @@ -177,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(); } @@ -207,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/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 94% rename from lib/src/objects/parse_object.dart rename to packages/dart/lib/src/objects/parse_object.dart index 94d9a1576..35a9390c3 100644 --- a/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/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 93% rename from lib/src/objects/parse_session.dart rename to packages/dart/lib/src/objects/parse_session.dart index c02e1fe04..ab8f5105f 100644 --- a/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/lib/src/objects/parse_user.dart b/packages/dart/lib/src/objects/parse_user.dart similarity index 79% rename from lib/src/objects/parse_user.dart rename to packages/dart/lib/src/objects/parse_user.dart index 55c822e71..6429d3c3d 100644 --- a/lib/src/objects/parse_user.dart +++ b/packages/dart/lib/src/objects/parse_user.dart @@ -21,15 +21,15 @@ class ParseUser extends ParseObject implements ParseCloneable { securityContext: ParseCoreData().securityContext); this.username = username; - this.password = password; this.emailAddress = emailAddress; + this.password = password; this.sessionToken = sessionToken; } 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 +40,17 @@ class ParseUser extends ParseObject implements ParseCloneable { static const String keyEmailAddress = 'email'; static const String path = '$keyEndPointClasses$keyClassUser'; + 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); set acl(Map acl) => @@ -54,10 +65,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) => @@ -118,7 +125,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) { @@ -143,25 +151,34 @@ 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(); 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 = 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); @@ -186,11 +203,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); @@ -207,16 +226,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); @@ -238,14 +259,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); @@ -268,8 +291,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); @@ -292,9 +318,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) { @@ -306,9 +333,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) { @@ -356,7 +384,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) { @@ -379,7 +408,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; @@ -407,12 +437,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/lib/src/objects/response/parse_error_response.dart b/packages/dart/lib/src/objects/response/parse_error_response.dart similarity index 70% rename from lib/src/objects/response/parse_error_response.dart rename to packages/dart/lib/src/objects/response/parse_error_response.dart index f7a50a623..e73360709 100644 --- a/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/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 96% rename from lib/src/objects/response/parse_response_builder.dart rename to packages/dart/lib/src/objects/response/parse_response_builder.dart index cf6ff8f14..41631ac43 100644 --- a/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/lib/src/objects/response/parse_response_utils.dart b/packages/dart/lib/src/objects/response/parse_response_utils.dart similarity index 79% rename from lib/src/objects/response/parse_response_utils.dart rename to packages/dart/lib/src/objects/response/parse_response_utils.dart index 0201ee539..864e63654 100644 --- a/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/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/data/core_store.dart b/packages/dart/lib/src/storage/core_store.dart similarity index 100% rename from lib/src/data/core_store.dart rename to packages/dart/lib/src/storage/core_store.dart 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/lib/src/storage/core_store_sem_impl.dart b/packages/dart/lib/src/storage/core_store_sem_impl.dart similarity index 81% rename from lib/src/storage/core_store_sem_impl.dart rename to packages/dart/lib/src/storage/core_store_sem_impl.dart index f8e2c317d..466fc24ec 100644 --- a/lib/src/storage/core_store_sem_impl.dart +++ b/packages/dart/lib/src/storage/core_store_sem_impl.dart @@ -6,17 +6,19 @@ 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); + 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/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 99% rename from lib/src/utils/parse_file_extensions.dart rename to packages/dart/lib/src/utils/parse_file_extensions.dart index 6482c2c41..d0454b691 100644 --- a/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/lib/src/utils/parse_live_list.dart b/packages/dart/lib/src/utils/parse_live_list.dart similarity index 74% rename from lib/src/utils/parse_live_list.dart rename to packages/dart/lib/src/utils/parse_live_list.dart index 0528e676d..fd29f9235 100644 --- a/lib/src/utils/parse_live_list.dart +++ b/packages/dart/lib/src/utils/parse_live_list.dart @@ -1,39 +1,48 @@ -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 { - 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) { @@ -88,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; } @@ -132,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(); } @@ -335,8 +336,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; @@ -410,11 +411,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 @@ -447,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); @@ -678,8 +692,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,254 +749,19 @@ 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}); + ParseLiveListElementSnapshot( + {this.loadedData, this.error, this.preLoadedData}); final T loadedData; + final T preLoadedData; + final ParseError error; bool get hasData => loadedData != null; - 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; + bool get hasPreLoadedData => preLoadedData != null; - @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; - } + bool get failed => error != null; } diff --git a/lib/src/utils/parse_logger.dart b/packages/dart/lib/src/utils/parse_logger.dart similarity index 71% rename from lib/src/utils/parse_logger.dart rename to packages/dart/lib/src/utils/parse_logger.dart index cb10ef8f5..2a0530d73 100644 --- a/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/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 93% rename from lib/src/utils/parse_utils.dart rename to packages/dart/lib/src/utils/parse_utils.dart index 0fa7b4c2b..543521b24 100644 --- a/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'); @@ -84,3 +85,7 @@ Future batchRequest( return handleException(e, ParseApiRQ.batch, debug, 'parse_utils'); } } + +Stream _createStreamError(Object error) async* { + throw error; +} diff --git a/pubspec.yaml b/packages/dart/pubspec.yaml similarity index 55% rename from pubspec.yaml rename to packages/dart/pubspec.yaml index 513807a96..d78d35b1a 100644 --- a/pubspec.yaml +++ b/packages/dart/pubspec.yaml @@ -1,32 +1,27 @@ name: parse_server_sdk -description: Flutter plugin for Parse Server, (https://parseplatform.org), (https://back4app.com) -version: 1.0.27 +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 - flutter: - sdk: flutter # Networking + dio: ^3.0.10 web_socket_channel: ^1.1.0 - connectivity: ^0.4.9+2 #Database sembast: ^2.4.7+6 + sembast_web: ^1.2.0 xxtea: ^2.0.3 - shared_preferences: ^0.5.10 # Utils - 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 + mime_type: ^0.3.2 dev_dependencies: # Testing diff --git a/test/parse_client_configuration_test.dart b/packages/dart/test/parse_client_configuration_test.dart similarity index 70% rename from test/parse_client_configuration_test.dart rename to packages/dart/test/parse_client_configuration_test.dart index 237c7fd79..f55f2ebf1 100644 --- a/test/parse_client_configuration_test.dart +++ b/packages/dart/test/parse_client_configuration_test.dart @@ -1,26 +1,29 @@ 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()); - test('testBuilder', () async { await Parse().initialize('appId', 'serverUrl', clientKey: 'clientKey', liveQueryUrl: 'liveQueryUrl', appName: 'appName', + appPackageName: 'somePackageName', + appVersion: 'someAppVersion', masterKey: 'masterKey', sessionId: 'sessionId', - debug: true); + 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..c95c373c8 --- /dev/null +++ b/packages/dart/test/parse_query_test.dart @@ -0,0 +1,44 @@ +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 = + Uri.parse(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/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. + +======= diff --git a/packages/flutter/README.md b/packages/flutter/README.md new file mode 100644 index 000000000..5ee3de12e --- /dev/null +++ b/packages/flutter/README.md @@ -0,0 +1,928 @@ +

+ Parse Logo + Flutter Logo +

+ +--- + +## 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) + +## 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 +``` + +⚠️ 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. +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 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 +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. + +### 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 +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. + +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){ + 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 97% rename from example/pubspec.yaml rename to packages/flutter/example/pubspec.yaml index 620f1db5f..666274921 100644 --- a/example/pubspec.yaml +++ b/packages/flutter/example/pubspec.yaml @@ -11,8 +11,10 @@ dependencies: sembast: ^2.0.1 shared_preferences: ^0.5.0 + 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/packages/flutter/lib/parse_server_sdk.dart b/packages/flutter/lib/parse_server_sdk.dart new file mode 100644 index 000000000..92950b138 --- /dev/null +++ b/packages/flutter/lib/parse_server_sdk.dart @@ -0,0 +1,205 @@ +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'; + +export 'package:parse_server_sdk/parse_server_sdk.dart' + hide Parse, CoreStoreSembastImp; + +part 'src/storage/core_store_sp_impl.dart'; +part 'src/utils/parse_live_list.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); + /// ``` + /// [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 appVersion, + String appPackageName, + String locale, + String liveQueryUrl, + String clientKey, + String masterKey, + String sessionId, + bool autoSendSessionId, + SecurityContext securityContext, + sdk.CoreStore coreStore, + Map registeredSubClassMap, + sdk.ParseUserConstructor parseUserConstructor, + sdk.ParseFileConstructor parseFileConstructor, + List liveListRetryIntervals, + sdk.ParseConnectivityProvider connectivityProvider, + String fileDirectory, + Stream appResumedStream, + }) async { + if (!sdk.parseIsWeb && + (appName == null || appVersion == null || appPackageName == null)) { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appName ??= packageInfo.appName; + appVersion ??= packageInfo.version; + appPackageName ??= packageInfo.packageName; + } + + return await super.initialize( + appId, + serverUrl, + debug: debug, + appName: appName, + appVersion: appVersion, + appPackageName: appPackageName, + locale: locale ?? sdk.parseIsWeb + ? ui.window.locale.toString() + : Platform.localeName, + liveQueryUrl: liveQueryUrl, + clientKey: clientKey, + masterKey: masterKey, + sessionId: sessionId, + autoSendSessionId: autoSendSessionId, + securityContext: securityContext, + coreStore: coreStore ?? + await CoreStoreSharedPrefsImp.getInstance(password: masterKey), + registeredSubClassMap: registeredSubClassMap, + parseUserConstructor: parseUserConstructor, + parseFileConstructor: parseFileConstructor, + liveListRetryIntervals: liveListRetryIntervals, + connectivityProvider: connectivityProvider ?? this, + fileDirectory: fileDirectory ?? + (!sdk.parseIsWeb ? (await getTemporaryDirectory()).path : null), + appResumedStream: appResumedStream ?? _appResumedStreamController.stream, + ); + } + + 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; + } + + @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; + } + }); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _appResumedStreamController.sink.add(null); + } +} + +class CoreStoreSembastImp implements sdk.CoreStoreSembastImp { + CoreStoreSembastImp._(); + + static sdk.CoreStoreSembastImp _sembastImp; + + static Future getInstance( + {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._(); + } + + @override + Future clear() => _sembastImp.clear(); + + @override + Future containsKey(String key) => _sembastImp.containsKey(key); + + @override + Future get(String key) => _sembastImp.get(key); + + @override + Future getBool(String key) => _sembastImp.getBool(key); + + @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/src/storage/core_store_sp_impl.dart b/packages/flutter/lib/src/storage/core_store_sp_impl.dart similarity index 93% rename from lib/src/storage/core_store_sp_impl.dart rename to packages/flutter/lib/src/storage/core_store_sp_impl.dart index c09e6970a..63f54de44 100644 --- a/lib/src/storage/core_store_sp_impl.dart +++ b/packages/flutter/lib/src/storage/core_store_sp_impl.dart @@ -1,11 +1,11 @@ -part of flutter_parse_sdk; +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/packages/flutter/lib/src/utils/parse_live_list.dart b/packages/flutter/lib/src/utils/parse_live_list.dart new file mode 100644 index 000000000..0fca66328 --- /dev/null +++ b/packages/flutter/lib/src/utils/parse_live_list.dart @@ -0,0 +1,271 @@ +part of flutter_parse_sdk_flutter; + +typedef ChildBuilder = Widget Function( + BuildContext context, sdk.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, + this.preloadedColumns, + }) : super(key: key); + + final sdk.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; + final List preloadedColumns; + + @override + _ParseLiveListWidgetState createState() => _ParseLiveListWidgetState( + query: query, + removedItemBuilder: removedItemBuilder, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + preloadedColumns: preloadedColumns, + ); + + 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(sdk.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, + List preloadedColumns}) { + sdk.ParseLiveList.create( + query, + listenOnAllSubItems: listenOnAllSubItems, + listeningIncludes: listeningIncludes, + lazyLoading: lazyLoading, + preloadedColumns: preloadedColumns, + ).then((sdk.ParseLiveList value) { + setState(() { + _liveList = value; + _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 sdk.ParseLiveListDeleteEvent) { + _animatedListKey.currentState.removeItem( + event.index, + (BuildContext context, Animation animation) => + ParseLiveListElementWidget( + key: ValueKey(event.object?.get( + sdk.keyVarObjectId, + defaultValue: 'removingItem')), + childBuilder: widget.childBuilder ?? + ParseLiveListWidget.defaultChildBuilder, + sizeFactor: animation, + duration: widget.duration, + loadedData: () => event.object, + preLoadedData: () => event.object, + ), + duration: widget.duration); + } + }); + }); + }); + } + + final sdk.QueryBuilder query; + sdk.ParseLiveList _liveList; + final GlobalKey _animatedListKey = + GlobalKey(); + final ChildBuilder removedItemBuilder; + + @override + Widget build(BuildContext context) { + return _liveList == null + ? widget.listLoadingElement ?? Container() + : buildAnimatedList(); + } + + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + + 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), + preLoadedData: () => _liveList?.getPreLoadedAt(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, + this.preLoadedData, + @required this.sizeFactor, + @required this.duration, + @required this.childBuilder}) + : super(key: key); + + 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, preLoadedData, stream); + } +} + +class _ParseLiveListElementWidgetState + extends State> + with SingleTickerProviderStateMixin { + _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, preLoadedData: data); + }); + } else { + _snapshot = sdk.ParseLiveListElementSnapshot( + loadedData: data, preLoadedData: data); + } + }, + onError: (Object error) { + if (error is sdk.ParseError) { + if (widget != null) { + setState(() { + _snapshot = sdk.ParseLiveListElementSnapshot(error: error); + }); + } else { + _snapshot = sdk.ParseLiveListElementSnapshot(error: error); + } + } + }, + cancelOnError: false, + ); + } + } + + sdk.ParseLiveListElementSnapshot _snapshot; + + StreamSubscription _streamSubscription; + + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + + @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/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml new file mode 100644 index 000000000..16bfd4ee2 --- /dev/null +++ b/packages/flutter/pubspec.yaml @@ -0,0 +1,32 @@ +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 + +environment: + sdk: ">=2.2.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + parse_server_sdk: ^1.0.28 + + # Networking + dio: ^3.0.10 + connectivity: ^0.4.9+2 # only used in the flutter part + + #Database + shared_preferences: ^0.5.10 # only used in the flutter part + + # 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 + +dev_dependencies: + # Testing + flutter_test: + sdk: flutter + mockito: ^4.1.1 diff --git a/packages/flutter/test/parse_client_configuration_test.dart b/packages/flutter/test/parse_client_configuration_test.dart new file mode 100644 index 000000000..f2de6b5cc --- /dev/null +++ b/packages/flutter/test/parse_client_configuration_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + SharedPreferences.setMockInitialValues(Map()); + + 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'); + }); +} diff --git a/packages/flutter/test/parse_query_test.dart b/packages/flutter/test/parse_query_test.dart new file mode 100644 index 000000000..7883c4b36 --- /dev/null +++ b/packages/flutter/test/parse_query_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:parse_server_sdk_flutter/parse_server_sdk.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MockClient extends Mock implements ParseHTTPClient {} + +void main() { + SharedPreferences.setMockInitialValues(Map()); + + 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 = + Uri.parse(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/test/parse_query_test.dart b/test/parse_query_test.dart deleted file mode 100644 index 61c67005b..000000000 --- a/test/parse_query_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -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 {} - -void main() { - SharedPreferences.setMockInitialValues(Map()); - - group('queryBuilder', () { - test('whereRelatedTo', () async { - final MockClient client = MockClient(); - - await Parse().initialize('appId', 'https://test.parse.com', debug: true); - - 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); - }); - }); -}