From e992ab9a15095d5b97ea0a6dfc936d5777fc4e4f Mon Sep 17 00:00:00 2001 From: appeto dev Date: Tue, 26 May 2026 18:49:48 +0330 Subject: [PATCH] start --- .github/ISSUE_TEMPLATE/bug_report.md | 24 + .github/workflows/dart.yml | 27 + .gitignore | 9 + .metadata | 10 + .vscode/launch.json | 19 + CHANGELOG.md | 27 + LICENSE | 21 + README.md | 375 ++++++++++++ android/.gitignore | 8 + android/build.gradle | 47 ++ android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + android/settings.gradle | 1 + android/src/main/AndroidManifest.xml | 3 + .../pusher_client/PusherClientPlugin.kt | 23 + .../pusher_client/core/contracts/MChannel.kt | 8 + .../pusher_client/core/utils/Constants.kt | 12 + .../utils/JsonEncodedConnectionFactory.kt | 22 + .../pusher_client/pusher/PusherService.kt | 296 +++++++++ .../pusher/listeners/ConnectionListener.kt | 60 ++ .../FlutterBaseChannelEventListener.kt | 50 ++ .../listeners/FlutterChannelEventListener.kt | 7 + .../FlutterPresenceChannelEventListener.kt | 57 ++ .../FlutterPrivateChannelEventListener.kt | 30 + ...terPrivateEncryptedChannelEventListener.kt | 34 ++ example/.gitignore | 42 ++ example/.metadata | 10 + example/README.md | 16 + example/android/.gitignore | 11 + example/android/app/build.gradle | 63 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 47 ++ .../pusher_client_example/MainActivity.kt | 8 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + example/android/build.gradle | 31 + example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 6 + example/android/settings.gradle | 11 + example/ios/.gitignore | 32 + example/ios/Flutter/AppFrameworkInfo.plist | 26 + example/ios/Flutter/Debug.xcconfig | 2 + example/ios/Flutter/Release.xcconfig | 2 + example/ios/Podfile | 41 ++ example/ios/Podfile.lock | 41 ++ example/ios/Runner.xcodeproj/project.pbxproj | 575 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 ++ example/ios/Runner/Base.lproj/Main.storyboard | 26 + example/ios/Runner/Info.plist | 50 ++ example/ios/Runner/Runner-Bridging-Header.h | 1 + example/lib/main.dart | 109 ++++ example/pubspec.yaml | 71 +++ example/test/widget_test.dart | 27 + ios/.gitignore | 37 ++ ios/Assets/.gitkeep | 0 ios/Classes/AuthRequestBuilder.swift | 37 ++ ios/Classes/ChannelEventListener.swift | 28 + ios/Classes/ConnectionListener.swift | 45 ++ ios/Classes/Constants.swift | 24 + ios/Classes/MChannel.swift | 12 + ios/Classes/Models.swift | 102 ++++ ios/Classes/PusherClientPlugin.h | 4 + ios/Classes/PusherClientPlugin.m | 15 + ios/Classes/PusherService.swift | 221 +++++++ ios/Classes/StreamHandler.swift | 29 + ios/Classes/SwiftPusherClientPlugin.swift | 8 + ios/pusher_client.podspec | 28 + lib/pusher_client.dart | 7 + lib/src/contracts/stream_handler.dart | 29 + lib/src/models/connection_error.dart | 19 + lib/src/models/connection_error.g.dart | 18 + lib/src/models/connection_state_change.dart | 20 + lib/src/models/connection_state_change.g.dart | 18 + lib/src/models/event_stream_result.dart | 25 + lib/src/models/event_stream_result.g.dart | 26 + lib/src/pusher/channel.dart | 89 +++ lib/src/pusher/pusher_auth.dart | 18 + lib/src/pusher/pusher_auth.g.dart | 18 + lib/src/pusher/pusher_client.dart | 145 +++++ lib/src/pusher/pusher_client.g.dart | 13 + lib/src/pusher/pusher_event.dart | 29 + lib/src/pusher/pusher_event.g.dart | 20 + lib/src/pusher/pusher_options.dart | 63 ++ lib/src/pusher/pusher_options.g.dart | 34 ++ pubspec.yaml | 29 + pusher_client.iml | 19 + test/pusher_client_test.dart | 33 + 122 files changed, 4072 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/workflows/dart.yml create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 android/.gitignore create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/PusherClientPlugin.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/core/contracts/MChannel.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/Constants.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/JsonEncodedConnectionFactory.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/PusherService.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/ConnectionListener.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterBaseChannelEventListener.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterChannelEventListener.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPresenceChannelEventListener.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateChannelEventListener.kt create mode 100644 android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateEncryptedChannelEventListener.kt create mode 100644 example/.gitignore create mode 100644 example/.metadata create mode 100644 example/README.md create mode 100644 example/android/.gitignore create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/src/debug/AndroidManifest.xml create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/kotlin/com/github/chinloyal/pusher_client_example/MainActivity.kt create mode 100644 example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/app/src/profile/AndroidManifest.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 example/android/settings.gradle create mode 100644 example/ios/.gitignore create mode 100644 example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 example/ios/Flutter/Debug.xcconfig create mode 100644 example/ios/Flutter/Release.xcconfig create mode 100644 example/ios/Podfile create mode 100644 example/ios/Podfile.lock create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner/AppDelegate.swift create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 example/ios/Runner/Info.plist create mode 100644 example/ios/Runner/Runner-Bridging-Header.h create mode 100644 example/lib/main.dart create mode 100644 example/pubspec.yaml create mode 100644 example/test/widget_test.dart create mode 100644 ios/.gitignore create mode 100644 ios/Assets/.gitkeep create mode 100644 ios/Classes/AuthRequestBuilder.swift create mode 100644 ios/Classes/ChannelEventListener.swift create mode 100644 ios/Classes/ConnectionListener.swift create mode 100644 ios/Classes/Constants.swift create mode 100644 ios/Classes/MChannel.swift create mode 100644 ios/Classes/Models.swift create mode 100644 ios/Classes/PusherClientPlugin.h create mode 100644 ios/Classes/PusherClientPlugin.m create mode 100644 ios/Classes/PusherService.swift create mode 100644 ios/Classes/StreamHandler.swift create mode 100644 ios/Classes/SwiftPusherClientPlugin.swift create mode 100644 ios/pusher_client.podspec create mode 100644 lib/pusher_client.dart create mode 100644 lib/src/contracts/stream_handler.dart create mode 100644 lib/src/models/connection_error.dart create mode 100644 lib/src/models/connection_error.g.dart create mode 100644 lib/src/models/connection_state_change.dart create mode 100644 lib/src/models/connection_state_change.g.dart create mode 100644 lib/src/models/event_stream_result.dart create mode 100644 lib/src/models/event_stream_result.g.dart create mode 100644 lib/src/pusher/channel.dart create mode 100644 lib/src/pusher/pusher_auth.dart create mode 100644 lib/src/pusher/pusher_auth.g.dart create mode 100644 lib/src/pusher/pusher_client.dart create mode 100644 lib/src/pusher/pusher_client.g.dart create mode 100644 lib/src/pusher/pusher_event.dart create mode 100644 lib/src/pusher/pusher_event.g.dart create mode 100644 lib/src/pusher/pusher_options.dart create mode 100644 lib/src/pusher/pusher_options.g.dart create mode 100644 pubspec.yaml create mode 100644 pusher_client.iml create mode 100644 test/pusher_client_test.dart diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..734ba7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: chinloyal + +--- + +### Steps to reproduce + +What must I do to reproduce the bug? + +### Expected behavior + +What did you expect to happen? + +### Actual behavior + +What actually happened? Submit stack traces or anything that you think would help. + +### Any improvements you suggest + +... diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 0000000..f4dd69f --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,27 @@ +name: Publish pusher_client to pub.dev +on: + release: + types: + - published +jobs: + build: + runs-on: ubuntu-latest + container: + image: google/dart:latest + steps: + - uses: actions/checkout@v1 + - name: Setup credentials + run: | + mkdir -p ~/.pub-cache + cat < ~/.pub-cache/credentials.json + { + "accessToken":"${{ secrets.PUB_ACCESS_TOKEN }}", + "refreshToken":"${{ secrets.PUB_REFRESH_TOKEN }}", + "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", + "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], + "expiration": 1604281072748 + } + EOF + - name: Publish package + run: pub publish -f + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21b8f08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +.idea/ +.fvm/** \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..4129ce9 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 84f3d28555368a70270e9ac8390a9441df95e752 + channel: stable + +project_type: plugin diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f9b4261 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "pusher_client", + "request": "launch", + "type": "dart" + }, + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9c40e10 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +## 2.0.0 - August 31, 2021 + +* Upgraded to null safety [@nizwar](https://github.com/nizwar) + +## 1.1.3 - January 24, 2021 +* BUGFIX: Binding to channel events cancels calls to onConnectionStateChange +* Added a Stream Handler contract to allow multiple classes to listen to the same event channel + +## 1.1.2 - January 20, 2021 +* PATCH: Removed log4j dependency for android + +## 1.1.1 - January 19, 2021 +* BUGFIX: ConnectionEventListener null safety issue for Android (fixed by [@spiritinlife](https://github.com/spiritinlife)) + +## 1.1.0 - November 20, 2020 +* MINOR: Removed json_serializable and build_runner dependencies, this prevents conflict with projects that have to use those libraries at a specific version. + +## 1.0.2 - November 17, 2020 +* BUGFIX: Fixes issue #1, Double backslashes in the bind event breaks the JSON encoding. (This issue was only on android, iOS worked fine.) + +## 1.0.1 - November 7, 2020 + +* BUGFIX: Fixed not being able to bind to multiple types of channels at once. + +## 1.0.0 - October 26, 2020 + +* This is a complete Pusher Channels client library for flutter that fully supports Android and iOS with all the necessary and newest features such as having private encrypted channels. Triggering client event also works on both Android and iOS diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..937795b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Romario Chinloy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49e0dad --- /dev/null +++ b/README.md @@ -0,0 +1,375 @@ +# Pusher Channels Flutter Client + +[![pub version](https://img.shields.io/pub/v/pusher_client.svg?logo=dart)](https://pub.dartlang.org/packages/pusher_client) +[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/chinloyal/pusher_client/master/LICENSE) +![Languages](https://img.shields.io/badge/languages-dart%20%7C%20kotlin%20%7C%20swift-blueviolet.svg) +[![Twitter](https://img.shields.io/badge/twitter-@chinloyal-blue.svg?style=flat&logo=twitter)](https://twitter.com/chinloyal) + +A Pusher Channels client plugin for Flutter targeting Android and iOS. It wraps +[pusher-websocket-java](https://github.com/pusher/pusher-websocket-java) v2.2.5 and [pusher-websocket-swift](https://github.com/pusher/pusher-websocket-swift) v8.0.0. + +For tutorials and more in-depth information about Pusher Channels, visit the [official docs](https://pusher.com/docs/channels). + +This client works with official pusher servers and laravel self hosted pusher websocket server ([laravel-websockets](https://github.com/beyondcode/laravel-websockets)). + +## Supported Platforms & Deployment Targets + +- Android API 16 and above +- iOS 9.0 and above + +## Table of Contents + +- [Installation](#installation) +- [Configuration](#configuration) + - [For iOS](#for-ios) + - [For Android](#for-android) +- [API Overview](#api-overview) +- [The Pusher Constructor](#the-pusher-constructor) +- [Pusher Options Config](#pusher-options-config) +- [Reconnecting](#reconnecting) +- [Disconnecting](#disconnecting) +- [Subscribing To Channels](#subscribing-to-channels) + - [Public Channels](#public-channels) + - [Private Channels](#private-channels) + - [Private Encrypted Channels](#private-encrypted-channels) + - [Presence Channels](#presence-channels) +- [Binding To Events](#bindings-to-events) + - [Callback Parameters](#callback-parameters) + - [Unbind Channel Events](#unbind-channel-events) +- [Triggering Client Events](#triggering-client-events) +- [Accessing The Connection Socket ID](#accessing-the-connection-socket-id) +- [Resolve Common Issues](#resolve-common-issues) +## Installation + +Add to your pubspec.yaml + +```yaml +dependencies: + pusher_client: ^2.0.0 +``` + +## Configuration + +### For iOS + +Set the minimum deployment target in the Podfile to 9.0. Go to `ios/Podfile`, then uncomment this line: + +```Podfile +# platform :ios, '8.0' +``` + +Change it to: + +```Podfile +platform :ios, '9.0' +``` + +You may have an issue subscribing to private channels if you're using a local pusher server like [laravel-websockets](https://github.com/beyondcode/laravel-websockets), to fix this go to `ios/Runner/Info.plist` and add: + +```xml +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +``` + +If you know which domains you will connect to add: + +```xml +NSAppTransportSecurity + + NSExceptionDomains + + example.com + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + +``` + +### For Android + +If you have enabled code obfuscation with R8 or proguard, you need to add the following rule in `android/app/build.gradle`: + +```gradle +buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } +} +``` + +Then in `android/app/proguard-rules.pro`: + +```pro +-keep class com.github.chinloyal.pusher_client.** { *; } +``` + + +## API Overview + +Here's the API in a nutshell. + +```dart +PusherOptions options = PusherOptions( + host: 'example.com', + wsPort: 6001, + encrypted: false, + auth: PusherAuth( + 'http://example.com/auth', + headers: { + 'Authorization': 'Bearer $token', + }, + ), +); + +PusherClient pusher = PusherClient( + YOUR_APP_KEY, + options, + autoConnect: false +); + +// connect at a later time than at instantiation. +pusher.connect(); + +pusher.onConnectionStateChange((state) { + print("previousState: ${state.previousState}, currentState: ${state.currentState}"); +}); + +pusher.onConnectionError((error) { + print("error: ${error.message}"); +}); + +// Subscribe to a private channel +Channel channel = pusher.subscribe("private-orders"); + +// Bind to listen for events called "order-status-updated" sent to "private-orders" channel +channel.bind("order-status-updated", (PusherEvent event) { + print(event.data); +}); + +// Unsubscribe from channel +pusher.unsubscribe("private-orders"); + +// Disconnect from pusher service +pusher.disconnect(); + +``` +More information in reference format can be found below. + + +## The Pusher Constructor + +The constructor takes an application key which you can get from the app's API Access section in the Pusher Channels dashboard, and a pusher options object. + +```dart +PusherClient pusher = PusherClient(YOUR_APP_KEY, PusherOptions()); +``` + + +If you are going to use [private](https://pusher.com/docs/channels/using_channels/private-channels), [presence](https://pusher.com/docs/channels/using_channels/presence-channels) or [encrypted](https://pusher.com/docs/channels/using_channels/encrypted-channels) channels then you will need to provide a `PusherAuth` to be used when authenticating subscriptions. In order to do this you need to pass in a `PusherOptions` object which has had an `auth` set. + +```dart +PusherAuth auth = PusherAuth( + // for auth endpoint use full url + 'http://example.com/auth', + headers: { + 'Authorization': 'Bearer $token', + }, +); + +PusherOptions options = PusherOptions( + auth: auth +); + +PusherClient pusher = PusherClient(YOUR_APP_KEY, options); +``` + +To disable logging and auto connect do this: + +```dart +PusherClient pusher = PusherClient( + YOUR_APP_KEY, + options, + enableLogging: false, + autoConnect: false, +); +``` + +If auto connect is disabled then you can manually connect using `connect()` on the pusher instance. + +## Pusher Options Config + +Most of the functionality of this plugin is configured through the PusherOptions object. You configure it by setting parameters on the object before passing it to the Pusher client. Below is a table containing all of the properties you can set. + +| Method | Parameter | Description | +|-----------------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| encrypted | bool | Whether the connection should be made with TLS or not. | +| auth | PusherAuth | Sets the authorization options to be used when authenticating private, private-encrypted and presence channels. | +| host | String | The host to which connections will be made. | +| wsPort | int | The port to which unencrypted connections will be made. Automatically set correctly. | +| wssPort | int | The port to which encrypted connections will be made. Automatically set correctly. | +| cluster | String | Sets the cluster the client will connect to, thereby setting the Host and Port correctly. | +| activityTimeout | int | The number of milliseconds of inactivity at which a "ping" will be triggered to check the connection. The default value is 120,000. | +| pongTimeout | int | The number of milliseconds the client waits to receive a "pong" response from the server before disconnecting. The default value is 30,000. | +| maxReconnectionAttempts | int | Number of reconnection attempts that will be made when `pusher.connect()` is called, after which the client will give up. | +| maxReconnectGapInSeconds | int | The delay in two reconnection extends exponentially (1, 2, 4, .. seconds) This property sets the maximum inbetween two reconnection attempts. | + +## Reconnecting + +The `connect()` method is also used to re-connect in case the connection has been lost, for example if a device loses reception. Note that the state of channel subscriptions and event bindings will be preserved while disconnected and re-negotiated with the server once a connection is re-established. + +## Disconnecting + +```dart +pusher.disconnect(); +``` + +After disconnection the `PusherClient` instance will release any internally allocated resources (threads and network connections) + +## Subscribing To Channels + +Channels use the concept of [channels](https://pusher.com/docs/channels/using_channels/channels) as a way of subscribing to data. They are identified and subscribed to by a simple name. Events are bound to a channel and are also identified by name. + +As mentioned above, channel subscriptions need only be registered once by the `PusherClient` instance. They are preserved across disconnection and re-established with the server on reconnect. They should NOT be re-registered. They may, however, be registered with a `PusherClient` instance before the first call to `connect` - they will be completed with the server as soon as a connection becomes available. + +### Public Channels + +The default method for subscribing to a channel involves invoking the subscribe method of your client object: + +```dart +Channel channel = pusher.subscribe("my-channel"); +``` +This returns a `Channel` object, which events can be bound to. + +### Private Channels + +Private channels are created in exactly the same way as public channels, except that they reside in the _'private-'_ namespace. This means prefixing the channel name: + +```dart +Channel privateChannel = pusher.subscribe("private-status-update"); +``` + +Subscribing to private channels involves the client being authenticated. See [The Pusher Constructor](#the-pusher-constructor) section for the authenticated channel example for more information. + +### Private Encrypted Channels + +Similar to Private channels, you can also subscribe to a private encrypted channel. This plugin fully supports end-to-end encryption. This means that only you and your connected clients will be able to read your messages. Pusher cannot decrypt them. These channels must be prefixed with _'private-encrypted-'_ + +Like with private channels, you must provide an authentication endpoint. That endpoint must be using a [server client that supports end-to-end encryption](https://pusher.com/docs/channels/using_channels/encrypted-channels#server). There is a [demonstration endpoint to look at using nodejs](https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption). + +### Presence Channels + +Presence channels are channels whose names are prefixed by _'presence-'_. Presence channels also need to be authenticated. + +```dart +Channel presenceChannel = pusher.subscribe("presence-another-channel"); +``` + +## Binding To Events + +There are two types of events that occur on channel subscriptions. + +1. Protocol related events such as those triggered when a subscription succeeds, for example "pusher:subscription_succeeded" +2. Application events that have been triggered by code within your app + +```dart +Channel channel = pusher.subscribe("private-orders"); + +channel.bind("order-status-updated", (PusherEvent event) { + print(event.data); +}); +``` + +### Callback Parameters + +The callbacks you bind receive a `PusherEvent`: + +| Property | Type | Description | +| ------------------ |--------------| ------------ +| `eventName` | `String` | The name of the event. | +| `channelName` | `String` | The name of the channel that the event was triggered on. (Optional) | +| `data` | `String` | The data that was passed to `trigger`, encoded as a string. If you passed an object then that will have been serialized to a JSON string which you can parse as necessary. (Optional)| +| `userId` | `String` | The ID of the user who triggered the event. This is only available for client events triggered on presence channels. (Optional)| + +### Unbind Channel Events + +You can unbind from an event by doing: + +```dart +channel.unbind("order-status-updated"); +``` + +## Triggering Client Events + +Once a private or presence subscription has been authorized and the subscription has succeeded, it is possible to trigger events on those channels. + +Events triggered by clients are called [client events](https://pusher.com/docs/channels/using_channels/events#triggering-client-events). Because they are being triggered from a client which may not be trusted there are a number of enforced rules when using them. Some of these rules include: + +- Event names must have a _'client-'_ prefix +- Rate limits +- You can only trigger an event when the subscription has succeeded + +```dart +channel.bind("pusher:subscription_succeeded", (PusherEvent event) { + channel.trigger("client-istyping", {"name": "Bob"}); +}); +``` + +For full details see the [client events documentation](https://pusher.com/docs/channels/using_channels/events#triggering-client-events). + + +## Accessing The Connection Socket ID + +Once connected you can access a unique identifier for the current client's connection. This is known as the **socket Id**. You can access the value once the connection has been established as follows: + +```dart +String socketId = pusher.getSocketId(); +``` + +For more information on how and why there is a socket Id see the documentation on authenticating users and [excluding recipients](https://pusher.com/docs/channels/server_api/excluding-event-recipients). + +## Resolve Common Issues + +### iOS doesn't log when enableLogging is set to true + +iOS logging doesn't seem to output to flutter console, however if you run the +app from Xcode you should be able to see the logs. + + +### Subscribing to private channels with iOS + +If using a local pusher server but are unable to subscribe to a private channel +then add this to your ios/Runner/Info.plist: + +```xml +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +``` + +If you know which domains you will connect to add: + +```xml +NSAppTransportSecurity + + NSExceptionDomains + + example.com + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + +``` diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..34cb8aa --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,47 @@ +group 'com.github.chinloyal.pusher_client' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.8.0' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } + + dependencies { + implementation "com.pusher:pusher-java-client:2.2.5" + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..01a286e --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..20ec558 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pusher_client' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1d32591 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/PusherClientPlugin.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/PusherClientPlugin.kt new file mode 100644 index 0000000..4a3df03 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/PusherClientPlugin.kt @@ -0,0 +1,23 @@ +package com.github.chinloyal.pusher_client + +import androidx.annotation.NonNull +import com.github.chinloyal.pusher_client.pusher.PusherService + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar + +/** PusherClientPlugin */ +class PusherClientPlugin: FlutterPlugin { + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + PusherService().register(flutterPluginBinding.binaryMessenger); + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { +// channel.setMethodCallHandler(null) + } +} diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/contracts/MChannel.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/contracts/MChannel.kt new file mode 100644 index 0000000..9757d50 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/contracts/MChannel.kt @@ -0,0 +1,8 @@ +package com.github.chinloyal.pusher_client.core.contracts + +import androidx.annotation.NonNull +import io.flutter.plugin.common.BinaryMessenger + +interface MChannel { + fun register (@NonNull messenger: BinaryMessenger) +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/Constants.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/Constants.kt new file mode 100644 index 0000000..aa1f4fa --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/Constants.kt @@ -0,0 +1,12 @@ +package com.github.chinloyal.pusher_client.core.utils + +enum class Constants(val value: String) { + CONNECTION_ESTABLISHED("pusher:connection_established"), + ERROR("pusher:error"), + SUBSCRIBE("pusher:subscribe"), + UNSUBSCRIBE("pusher:unsubscribe"), + SUBSCRIPTION_ERROR("pusher:subscription_error"), + SUBSCRIPTION_SUCCEEDED("pusher:subscription_succeeded"), + MEMBER_ADDED("pusher:member_added"), + MEMBER_REMOVED("pusher:member_removed"), +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/JsonEncodedConnectionFactory.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/JsonEncodedConnectionFactory.kt new file mode 100644 index 0000000..e668df1 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/core/utils/JsonEncodedConnectionFactory.kt @@ -0,0 +1,22 @@ +package com.github.chinloyal.pusher_client.core.utils + +import com.pusher.client.util.ConnectionFactory +import org.json.JSONObject + +class JsonEncodedConnectionFactory : ConnectionFactory() { + override fun getCharset(): String { + return "UTF-8"; + } + + override fun getContentType(): String { + return "application/json"; + } + + override fun getBody(): String { + val data: JSONObject = JSONObject(); + data.put("channel_name", channelName); + data.put("socket_id", socketId); + + return data.toString(); + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/PusherService.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/PusherService.kt new file mode 100644 index 0000000..5fea015 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/PusherService.kt @@ -0,0 +1,296 @@ +package com.github.chinloyal.pusher_client.pusher + +import android.util.Log +import com.github.chinloyal.pusher_client.core.contracts.MChannel +import com.github.chinloyal.pusher_client.core.utils.JsonEncodedConnectionFactory +import com.github.chinloyal.pusher_client.pusher.listeners.* +import com.google.gson.Gson +import com.pusher.client.Pusher +import com.pusher.client.PusherOptions +import com.pusher.client.connection.ConnectionState +import com.pusher.client.util.HttpAuthorizer +import com.pusher.client.util.UrlEncodedConnectionFactory +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result +import org.json.JSONObject +import java.lang.Exception + + +const val CHANNEL_NAME = "com.github.chinloyal/pusher_client" +const val EVENT_STREAM = "com.github.chinloyal/pusher_client_stream" +const val LOG_TAG = "PusherClientPlugin" +const val PRIVATE_PREFIX = "private-" +const val PRIVATE_ENCRYPTED_PREFIX = "private-encrypted-" +const val PRESENCE_PREFIX = "presence-" + +class PusherService : MChannel { + private var _pusherInstance: Pusher? = null + + companion object { + var enableLogging: Boolean = false + var eventSink: EventSink? = null + fun debugLog(msg: String) { + if(enableLogging) { + Log.d(LOG_TAG, msg) + } + } + + fun errorLog(msg: String) { + if(enableLogging) { + Log.e(LOG_TAG, msg) + } + } + } + + override fun register(messenger: BinaryMessenger) { + MethodChannel(messenger, CHANNEL_NAME).setMethodCallHandler { call, result -> + when (call.method) { + "init" -> init(call, result) + "connect" -> connect(result) + "disconnect" -> disconnect(result) + "getSocketId" -> getSocketId(result) + "subscribe" -> subscribe(call, result) + "unsubscribe" -> unsubscribe(call, result) + "bind" -> bind(call, result) + "unbind" -> unbind(call, result) + "trigger" -> trigger(call, result) + else -> result.notImplemented() + } + } + + EventChannel(messenger, EVENT_STREAM).setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(args: Any?, eventSink: EventSink) { + PusherService.eventSink = eventSink + debugLog("Event stream listening...") + } + + override fun onCancel(args: Any?) { + debugLog("Event stream cancelled.") + } + }) + } + + private fun init(call: MethodCall, result: Result) { + // toString works because this is json encoded in dart + val args = JSONObject(call.arguments.toString()) + val initArgs: JSONObject = args.getJSONObject("initArgs") + enableLogging = initArgs.getBoolean("enableLogging") + + if(_pusherInstance == null) { + val options: JSONObject = args.getJSONObject("pusherOptions") + val pusherOptions = PusherOptions() + + if (!options.isNull("auth")) { + val auth: JSONObject = options.getJSONObject("auth") + val endpoint: String = auth.getString("endpoint") + val headersMap: Map = Gson().fromJson>(auth.getString("headers"), Map::class.java) + val encodedConnectionFactory = if (headersMap.containsValue("application/json")) + JsonEncodedConnectionFactory() else UrlEncodedConnectionFactory() + + val authorizer = HttpAuthorizer(endpoint, encodedConnectionFactory) + authorizer.setHeaders(headersMap) + + pusherOptions.authorizer = authorizer + } + + pusherOptions.setHost(options.getString("host")) + + if(!options.isNull("cluster")) { + pusherOptions.setCluster(options.getString("cluster")) + } + + pusherOptions.activityTimeout = options.getLong("activityTimeout") + pusherOptions.pongTimeout = options.getLong("pongTimeout") + pusherOptions.maxReconnectionAttempts = options.getInt("maxReconnectionAttempts") + pusherOptions.maxReconnectGapInSeconds = options.getInt("maxReconnectGapInSeconds") + pusherOptions.setWsPort(options.getInt("wsPort")) + pusherOptions.setWssPort(options.getInt("wssPort")) + pusherOptions.isUseTLS = options.getBoolean("encrypted") + + _pusherInstance = Pusher(args.getString("appKey"), pusherOptions) + + debugLog("Pusher initialized") + } + + result.success(null) + } + + private fun connect(result: Result) { + _pusherInstance?.connect(ConnectionListener(), ConnectionState.ALL) + result.success(null) + } + + private fun disconnect(result: Result) { + _pusherInstance?.disconnect() + debugLog("Disconnect") + result.success(null) + } + + private fun getSocketId(result: Result) { + result.success(_pusherInstance?.connection?.socketId) + } + + private fun subscribe(call: MethodCall, result: Result) { + try { + val src = call.arguments as Map + val args = JSONObject(src) + val channelName: String = args.getString("channelName") + + when { + channelName.startsWith(PRIVATE_ENCRYPTED_PREFIX) -> { + val channel = _pusherInstance?.getPrivateEncryptedChannel(channelName) + if (channel == null || !channel.isSubscribed) + _pusherInstance?.subscribePrivateEncrypted(channelName, FlutterPrivateEncryptedChannelEventListener.instance) + } + channelName.startsWith(PRIVATE_PREFIX) -> { + val channel = _pusherInstance?.getPrivateChannel(channelName) + if (channel == null || !channel.isSubscribed) + _pusherInstance?.subscribePrivate(channelName, FlutterPrivateChannelEventListener.instance) + } + channelName.startsWith(PRESENCE_PREFIX) -> { + val channel = _pusherInstance?.getPresenceChannel(channelName) + if (channel == null || !channel.isSubscribed) + _pusherInstance?.subscribePresence(channelName, FlutterPresenceChannelEventListener.instance) + } + else -> { + val channel = _pusherInstance?.getChannel(channelName) + if (channel == null || !channel.isSubscribed) + _pusherInstance?.subscribe(channelName, FlutterChannelEventListener.instance) + } + } + + result.success(null) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) e.printStackTrace() + result.error("SUBSCRIBE_ERROR", e.message, e) + } + } + + private fun unsubscribe(call: MethodCall, result: Result) { + try { + val src = call.arguments as Map + val args = JSONObject(src); + val channelName = args.getString("channelName") + + _pusherInstance?.unsubscribe(channelName) + + debugLog("Unsubscribed: $channelName") + result.success(null) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) e.printStackTrace() + result.error("UNSUBSCRIBE_ERROR", e.message, e) + } + } + + /** + * Note binding can happen before the channel has been subscribed to + */ + private fun bind(call: MethodCall, result: Result) { + try { + val src = call.arguments as Map + val args = JSONObject(src) + val channelName: String = args.getString("channelName") + val eventName: String = args.getString("eventName") + + when { + channelName.startsWith(PRIVATE_ENCRYPTED_PREFIX) -> { + val channel = _pusherInstance?.getPrivateEncryptedChannel(channelName) + channel?.bind(eventName, FlutterPrivateEncryptedChannelEventListener.instance) + } + channelName.startsWith(PRIVATE_PREFIX) -> { + val channel = _pusherInstance?.getPrivateChannel(channelName) + channel?.bind(eventName, FlutterPrivateChannelEventListener.instance) + } + channelName.startsWith(PRESENCE_PREFIX) -> { + val channel = _pusherInstance?.getPresenceChannel(channelName) + channel?.bind(eventName, FlutterPresenceChannelEventListener.instance) + } + else -> { + val channel = _pusherInstance?.getChannel(channelName) + channel?.bind(eventName, FlutterChannelEventListener.instance) + } + } + + debugLog("[BIND] $eventName") + result.success(null) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) e.printStackTrace() + result.error("BIND_ERROR", e.message, e) + } + } + + private fun unbind(call: MethodCall, result: Result) { + try { + val src = call.arguments as Map + val args = JSONObject(src) + val channelName: String = args.getString("channelName") + val eventName: String = args.getString("eventName") + + when { + channelName.startsWith(PRIVATE_ENCRYPTED_PREFIX) -> { + val channel = _pusherInstance?.getPrivateEncryptedChannel(channelName) + channel?.unbind(eventName, FlutterPrivateEncryptedChannelEventListener.instance) + } + channelName.startsWith(PRIVATE_PREFIX) -> { + val channel = _pusherInstance?.getPrivateChannel(channelName) + channel?.unbind(eventName, FlutterPrivateChannelEventListener.instance) + } + channelName.startsWith(PRESENCE_PREFIX) -> { + val channel = _pusherInstance?.getPresenceChannel(channelName) + channel?.unbind(eventName, FlutterPresenceChannelEventListener.instance) + } + else -> { + val channel = _pusherInstance?.getChannel(channelName) + channel?.unbind(eventName, FlutterChannelEventListener.instance) + } + } + + debugLog("[UNBIND] $eventName") + result.success(null) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) e.printStackTrace() + result.error("UNBIND_ERROR", e.message, e) + } + } + + private fun trigger(call: MethodCall, result: Result) { + try { + // toString works because this is json encoded in dart + val args = JSONObject(call.arguments.toString()) + val eventName: String = args.getString("eventName") + val data: String = args.getString("data") + val channelName: String = args.getString("channelName") + val errorMessage = "Trigger can only be called on private and presence channels." + when { + channelName.startsWith(PRIVATE_ENCRYPTED_PREFIX) -> { + result.error("TRIGGER_ERROR", errorMessage, null) + } + channelName.startsWith(PRIVATE_PREFIX) -> { + val channel = _pusherInstance?.getPrivateChannel(channelName) + channel?.trigger(eventName, data) + } + channelName.startsWith(PRESENCE_PREFIX) -> { + val channel = _pusherInstance?.getPresenceChannel(channelName) + channel?.trigger(eventName, data) + } + else -> result.error("TRIGGER_ERROR", errorMessage, null) + } + + debugLog("[TRIGGER] $eventName") + result.success(null) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) e.printStackTrace() + result.error("TRIGGER_ERROR", e.message, e) + } + } + +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/ConnectionListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/ConnectionListener.kt new file mode 100644 index 0000000..52f1cf6 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/ConnectionListener.kt @@ -0,0 +1,60 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +import android.os.Handler +import android.os.Looper +import com.github.chinloyal.pusher_client.pusher.PusherService +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.debugLog +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.enableLogging +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.errorLog +import com.pusher.client.connection.ConnectionEventListener +import com.pusher.client.connection.ConnectionStateChange +import org.json.JSONObject +import java.lang.Exception + +class ConnectionListener: ConnectionEventListener { + val eventStreamJson = JSONObject(); + + override fun onConnectionStateChange(change: ConnectionStateChange) { + Handler(Looper.getMainLooper()).post { + try { + val connectionStateChange = JSONObject(mapOf( + "currentState" to change.currentState.toString(), + "previousState" to change.previousState.toString() + )) + + debugLog("[${change.currentState.toString()}]") + + eventStreamJson.put("connectionStateChange", connectionStateChange) + + PusherService.eventSink?.success(eventStreamJson.toString()) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (enableLogging) { + e.printStackTrace() + } + } + } + } + + override fun onError(message: String, code: String?, ex: Exception?) { + Handler(Looper.getMainLooper()).post { + try { + val connectionError = JSONObject(mapOf( + "message" to message, + "code" to code, + "exception" to ex?.message + )) + + debugLog("[ON_ERROR]: message: $message, code: $code") + + eventStreamJson.put("connectionError", connectionError) + PusherService.eventSink?.success(eventStreamJson.toString()) + } catch (e: Exception) { + e.message?.let { errorLog(it) } + if (PusherService.enableLogging) { + e.printStackTrace() + } + } + } + } +} diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterBaseChannelEventListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterBaseChannelEventListener.kt new file mode 100644 index 0000000..9c72f14 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterBaseChannelEventListener.kt @@ -0,0 +1,50 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +import android.os.Handler +import android.os.Looper +import com.github.chinloyal.pusher_client.core.utils.Constants +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.debugLog +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.eventSink +import com.pusher.client.channel.ChannelEventListener +import com.pusher.client.channel.PusherEvent +import org.json.JSONObject +import java.lang.Exception + +open class FlutterBaseChannelEventListener: ChannelEventListener { + private val eventStreamJson = JSONObject(); + + override fun onEvent(event: PusherEvent) { + Handler(Looper.getMainLooper()).post { + try { + val eventJson = JSONObject(mapOf( + "channelName" to event.channelName, + "eventName" to event.eventName, + "userId" to event.userId, + "data" to event.data + )) + + eventStreamJson.put("pusherEvent", eventJson) + + eventSink?.success(eventStreamJson.toString()) + debugLog(""" + |[ON_EVENT] Channel: ${event.channelName}, EventName: ${event.eventName}, + |Data: ${event.data}, User Id: ${event.userId} + """.trimMargin()) + } catch (e: Exception) { + eventSink?.error("ON_EVENT_ERROR", e.message, e) + } + + } + } + + override fun onSubscriptionSucceeded(channelName: String) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.SUBSCRIPTION_SUCCEEDED.value, + "channel" to channelName, + "user_id" to null, + "data" to null + ))) + debugLog("[PUBLIC] Subscribed: $channelName") + + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterChannelEventListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterChannelEventListener.kt new file mode 100644 index 0000000..a6c0099 --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterChannelEventListener.kt @@ -0,0 +1,7 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +class FlutterChannelEventListener: FlutterBaseChannelEventListener() { + companion object { + val instance = FlutterChannelEventListener() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPresenceChannelEventListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPresenceChannelEventListener.kt new file mode 100644 index 0000000..a5bc0ad --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPresenceChannelEventListener.kt @@ -0,0 +1,57 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +import com.github.chinloyal.pusher_client.core.utils.Constants +import com.github.chinloyal.pusher_client.pusher.PusherService +import com.pusher.client.channel.PresenceChannelEventListener +import com.pusher.client.channel.PusherEvent +import com.pusher.client.channel.User +import java.lang.Exception + +class FlutterPresenceChannelEventListener: FlutterBaseChannelEventListener(), PresenceChannelEventListener { + companion object { + val instance = FlutterPresenceChannelEventListener() + } + + override fun onUsersInformationReceived(channelName: String, users: MutableSet) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.SUBSCRIPTION_SUCCEEDED.value, + "channel" to channelName, + "user_id" to null, + "data" to users.toString() + ))) + } + + override fun userUnsubscribed(channelName: String, user: User) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.MEMBER_REMOVED.value, + "channel" to channelName, + "user_id" to user.id, + "data" to null + ))) + } + + override fun userSubscribed(channelName: String, user: User) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.MEMBER_ADDED.value, + "channel" to channelName, + "user_id" to user.id, + "data" to null + ))) + } + + override fun onAuthenticationFailure(message: String, e: Exception) { + PusherService.errorLog(message) + if(PusherService.enableLogging) e.printStackTrace() + } + + override fun onSubscriptionSucceeded(channelName: String) { + PusherService.debugLog("[PRESENCE] Subscribed: $channelName") + + this.onEvent(PusherEvent(mapOf( + "event" to Constants.SUBSCRIPTION_SUCCEEDED.value, + "channel" to channelName, + "user_id" to null, + "data" to null + ))) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateChannelEventListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateChannelEventListener.kt new file mode 100644 index 0000000..b376c6e --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateChannelEventListener.kt @@ -0,0 +1,30 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +import com.github.chinloyal.pusher_client.core.utils.Constants +import com.github.chinloyal.pusher_client.pusher.PusherService +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.enableLogging +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.errorLog +import com.pusher.client.channel.PrivateChannelEventListener +import com.pusher.client.channel.PusherEvent +import java.lang.Exception + +class FlutterPrivateChannelEventListener: FlutterBaseChannelEventListener(), PrivateChannelEventListener { + companion object { + val instance = FlutterPrivateChannelEventListener() + } + + override fun onAuthenticationFailure(message: String, e: Exception) { + errorLog(message) + if(enableLogging) e.printStackTrace() + } + + override fun onSubscriptionSucceeded(channelName: String) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.SUBSCRIPTION_SUCCEEDED.value, + "channel" to channelName, + "user_id" to null, + "data" to null + ))) + PusherService.debugLog("[PRIVATE] Subscribed: $channelName") + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateEncryptedChannelEventListener.kt b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateEncryptedChannelEventListener.kt new file mode 100644 index 0000000..142ef4f --- /dev/null +++ b/android/src/main/kotlin/com/github/chinloyal/pusher_client/pusher/listeners/FlutterPrivateEncryptedChannelEventListener.kt @@ -0,0 +1,34 @@ +package com.github.chinloyal.pusher_client.pusher.listeners + +import com.github.chinloyal.pusher_client.core.utils.Constants +import com.github.chinloyal.pusher_client.pusher.PusherService +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.enableLogging +import com.github.chinloyal.pusher_client.pusher.PusherService.Companion.errorLog +import com.pusher.client.channel.PrivateEncryptedChannelEventListener +import com.pusher.client.channel.PusherEvent +import java.lang.Exception + +class FlutterPrivateEncryptedChannelEventListener: FlutterBaseChannelEventListener(), PrivateEncryptedChannelEventListener { + companion object { + val instance = FlutterPrivateEncryptedChannelEventListener() + } + + override fun onDecryptionFailure(event: String, reason: String) { + errorLog("[PRIVATE-ENCRYPTED] Reason: $reason, Event: $event") + } + + override fun onAuthenticationFailure(message: String, e: Exception) { + errorLog(message) + if(enableLogging) e.printStackTrace() + } + + override fun onSubscriptionSucceeded(channelName: String) { + this.onEvent(PusherEvent(mapOf( + "event" to Constants.SUBSCRIPTION_SUCCEEDED.value, + "channel" to channelName, + "user_id" to null, + "data" to null + ))) + PusherService.debugLog("[PRIVATE-ENCRYPTED] Subscribed: $channelName") + } +} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..cb34ece --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json +testdata.txt \ No newline at end of file diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..24544cb --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 84f3d28555368a70270e9ac8390a9441df95e752 + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..7d01bf3 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# pusher_client_example + +Demonstrates how to use the pusher_client plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..bee277b --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,63 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.github.chinloyal.pusher_client_example" + minSdkVersion 16 + targetSdkVersion 29 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..4d6fabf --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6015e7f --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/github/chinloyal/pusher_client_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/github/chinloyal/pusher_client_example/MainActivity.kt new file mode 100644 index 0000000..b9a0be6 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/github/chinloyal/pusher_client_example/MainActivity.kt @@ -0,0 +1,8 @@ +package com.github.chinloyal.pusher_client_example + +import androidx.annotation.NonNull +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..1f83a33 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..4d6fabf --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..c505a86 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..a673820 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..18716eb --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Oct 24 14:05:31 EST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..f2872cf --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..252d9ec --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..fc297e3 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - Flutter (1.0.0) + - pusher_client (1.0.0): + - Flutter + - PusherSwiftWithEncryption (~> 8.0.0) + - PusherSwiftWithEncryption (8.0.0): + - ReachabilitySwift (~> 5.0) + - Sodium (= 0.8.0) + - Starscream (~> 3.1) + - ReachabilitySwift (5.0.0) + - Sodium (0.8.0) + - Starscream (3.1.1) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - pusher_client (from `.symlinks/plugins/pusher_client/ios`) + +SPEC REPOS: + trunk: + - PusherSwiftWithEncryption + - ReachabilitySwift + - Sodium + - Starscream + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + pusher_client: + :path: ".symlinks/plugins/pusher_client/ios" + +SPEC CHECKSUMS: + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + pusher_client: 98d16b50f0ebb4d67d3f61b29723142c03179da9 + PusherSwiftWithEncryption: 8ed9c9cf743b0530aa3a8c60de6734475f1aa67d + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + Sodium: 63c0ca312a932e6da481689537d4b35568841bdc + Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0 + +PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa + +COCOAPODS: 1.9.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..74da30c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,575 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 97F131E2C83FBF01E22B026A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3B8EFF3B144BD3E0F2EBA3A /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4E31BE8264BCB21079612752 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 586BC06C935CDA1810AF478C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A3B8EFF3B144BD3E0F2EBA3A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A43B87E7D061F4DC292BA84B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 97F131E2C83FBF01E22B026A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 228725705900A23A2E532A52 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A3B8EFF3B144BD3E0F2EBA3A /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3785AD25C8DE0DDC8BBCB2E4 /* Pods */ = { + isa = PBXGroup; + children = ( + 4E31BE8264BCB21079612752 /* Pods-Runner.debug.xcconfig */, + A43B87E7D061F4DC292BA84B /* Pods-Runner.release.xcconfig */, + 586BC06C935CDA1810AF478C /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 3785AD25C8DE0DDC8BBCB2E4 /* Pods */, + 228725705900A23A2E532A52 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DC132D9F9284A0E5E9F02590 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C3B00070C266A1CE0C53ECC3 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C3B00070C266A1CE0C53ECC3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DC132D9F9284A0E5E9F02590 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.github.chinloyal.pusherClientExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.github.chinloyal.pusherClientExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.github.chinloyal.pusherClientExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..31d8d1f --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..49f829f --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + pusher_client_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..61811e5 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,109 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:pusher_client/pusher_client.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + PusherClient pusher; + Channel channel; + + @override + void initState() { + super.initState(); + + String token = getToken(); + + pusher = new PusherClient( + "app-key", + PusherOptions( + // if local on android use 10.0.2.2 + host: 'localhost', + encrypted: false, + auth: PusherAuth( + 'http://example.com/broadcasting/auth', + headers: { + 'Authorization': 'Bearer $token', + }, + ), + ), + enableLogging: true, + ); + + channel = pusher.subscribe("private-orders"); + + pusher.onConnectionStateChange((state) { + log("previousState: ${state.previousState}, currentState: ${state.currentState}"); + }); + + pusher.onConnectionError((error) { + log("error: ${error.message}"); + }); + + channel.bind('status-update', (event) { + log(event.data); + }); + + channel.bind('order-filled', (event) { + log("Order Filled Event" + event.data.toString()); + }); + } + + String getToken() => "super-secret-token"; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Example Pusher App'), + ), + body: Center( + child: Column( + children: [ + ElevatedButton( + child: Text('Unsubscribe Private Orders'), + onPressed: () { + pusher.unsubscribe('private-orders'); + }, + ), + ElevatedButton( + child: Text('Unbind Status Update'), + onPressed: () { + channel.unbind('status-update'); + }, + ), + ElevatedButton( + child: Text('Unbind Order Filled'), + onPressed: () { + channel.unbind('order-filled'); + }, + ), + ElevatedButton( + child: Text('Bind Status Update'), + onPressed: () { + channel.bind('status-update', (PusherEvent event) { + log("Status Update Event" + event.data.toString()); + }); + }, + ), + ElevatedButton( + child: Text('Trigger Client Typing'), + onPressed: () { + channel.trigger('client-istyping', {'name': 'Bob'}); + }, + ), + ], + )), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..c25849d --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,71 @@ +name: pusher_client_example +description: Demonstrates how to use the pusher_client plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + pusher_client: + # When depending on this package from a real application you should use: + # pusher_client: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..a856418 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:pusher_client_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..aa479fd --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ios/Classes/AuthRequestBuilder.swift b/ios/Classes/AuthRequestBuilder.swift new file mode 100644 index 0000000..ba58478 --- /dev/null +++ b/ios/Classes/AuthRequestBuilder.swift @@ -0,0 +1,37 @@ +// +// AuthRequestBuilder.swift +// pusher_client +// +// Created by Romario Chinloy on 10/27/20. +// + +import PusherSwiftWithEncryption + +class AuthRequestBuilder: AuthRequestBuilderProtocol { + let pusherAuth: PusherAuth + + init(pusherAuth: PusherAuth) { + self.pusherAuth = pusherAuth + } + + func requestFor(socketID: String, channelName: String) -> URLRequest? { + var request = URLRequest(url: URL(string: pusherAuth.endpoint)!) + request.httpMethod = "POST" + do { + let authRequest = AuthRequest(socket_id: socketID, channel_name: channelName) + if(pusherAuth.headers.values.contains("application/json")) { + request.httpBody = try JSONEncoder().encode(authRequest) + } else { + request.httpBody = authRequest.valueUrlEncoded!.data(using: String.Encoding.utf8) + } + + pusherAuth.headers.forEach { (key: String, value: String) in + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } catch { + return nil + } + } +} diff --git a/ios/Classes/ChannelEventListener.swift b/ios/Classes/ChannelEventListener.swift new file mode 100644 index 0000000..7e6cb15 --- /dev/null +++ b/ios/Classes/ChannelEventListener.swift @@ -0,0 +1,28 @@ +// +// ChannelEventListener.swift +// pusher_client +// +// Created by Romario Chinloy on 10/31/20. +// + +import Foundation +import PusherSwiftWithEncryption + +class ChannelEventListener { + static let `default`: ChannelEventListener = ChannelEventListener() + + private init(){} + + func onEvent(event: PusherEvent) -> Void { + let pusherEvent = Event(eventName: event.eventName, channelName: event.channelName, userId: event.userId, data: event.data) + + do { + let data = try JSONEncoder().encode(EventStreamResult(pusherEvent: pusherEvent)) + StreamHandler.Utils.eventSink!(String(data: data, encoding: .utf8)) + + PusherService.Utils.debugLog(msg: "[ON_EVENT] Channel: \(event.channelName ?? ""), EventName: \(event.eventName), Data: \(event.data ?? ""), User Id: \(event.userId ?? "")") + } catch let err { + StreamHandler.Utils.eventSink!(FlutterError(code: "ON_EVENT_ERROR", message: err.localizedDescription, details: err)) + } + } +} diff --git a/ios/Classes/ConnectionListener.swift b/ios/Classes/ConnectionListener.swift new file mode 100644 index 0000000..707828d --- /dev/null +++ b/ios/Classes/ConnectionListener.swift @@ -0,0 +1,45 @@ +// +// ConnectionListener.swift +// pusher_client +// +// Created by Romario Chinloy on 10/27/20. +// + +import PusherSwiftWithEncryption + +class ConnectionListener: PusherDelegate { + static let `default`: ConnectionListener = ConnectionListener() + + private init(){} + + func changedConnectionState(from old: ConnectionState, to new: ConnectionState) { + do { + let connectionStateChange = ConnectionStateChange(currentState: new.stringValue(), previousState: old.stringValue()) + PusherService.Utils.debugLog(msg: "[\(new.stringValue())]") + let data = try JSONEncoder().encode(EventStreamResult(connectionStateChange: connectionStateChange)) + + StreamHandler.Utils.eventSink!(String(data: data, encoding: .utf8)) + + } catch let err { + PusherService.Utils.errorLog(msg: err.localizedDescription) + } + + } + + func receivedError(error: PusherError) { + do { + let connectionError = ConnectionError(message: error.message, code: String(describing: error.code), exception: error.description) + + PusherService.Utils.debugLog(msg: "[ON_ERROR]: message: \(error.message), code: \(String(describing: error.code))") + let data = try JSONEncoder().encode(EventStreamResult(connectionError: connectionError)) + StreamHandler.Utils.eventSink!(String(data: data, encoding: .utf8)) + } catch let err { + PusherService.Utils.errorLog(msg: err.localizedDescription) + } + + } + +// func debugLog(message: String) { +// PusherService.Utils.debugLog(msg: message) +// } +} diff --git a/ios/Classes/Constants.swift b/ios/Classes/Constants.swift new file mode 100644 index 0000000..1a68a8b --- /dev/null +++ b/ios/Classes/Constants.swift @@ -0,0 +1,24 @@ +// +// Constants.swift +// pusher_client +// +// Created by Romario Chinloy on 10/31/20. +// + +import Foundation + +public enum Constants { + enum Events: String, CaseIterable { + case connectionEstablished = "pusher:connection_established" + case error = "pusher:error" + case subscribe = "pusher:subscribe" + case unsubscribe = "pusher:unsubscribe" + case subscriptionError = "pusher:subscription_error" + case subscriptionSucceeded = "pusher:subscription_succeeded" + } + + enum PresenceEvents: String, CaseIterable { + case memberAdded = "pusher:member_added" + case memberRemoved = "pusher:member_removed" + } +} diff --git a/ios/Classes/MChannel.swift b/ios/Classes/MChannel.swift new file mode 100644 index 0000000..96aa983 --- /dev/null +++ b/ios/Classes/MChannel.swift @@ -0,0 +1,12 @@ +// +// MChannel.swift +// pusher_client +// +// Created by Romario Chinloy on 10/26/20. +// + +import Foundation + +protocol MChannel { + func register(messenger: FlutterBinaryMessenger) -> Void +} diff --git a/ios/Classes/Models.swift b/ios/Classes/Models.swift new file mode 100644 index 0000000..f148abe --- /dev/null +++ b/ios/Classes/Models.swift @@ -0,0 +1,102 @@ +// +// Models.swift +// pusher_client +// +// Created by Romario Chinloy on 10/27/20. +// + +struct PusherArgs: Codable { + var appKey: String + var pusherOptions: PusherOptions + var initArgs: InitArgs +} + +struct PusherOptions: Codable { + var auth: PusherAuth? + var cluster: String? + var host: String + var encrypted: Bool + var activityTimeout: Int + var pongTimeout: Int + var wsPort: Int + var wssPort: Int + var maxReconnectionAttempts: Int + var maxReconnectGapInSeconds: Int +} + +struct PusherAuth: Codable { + var endpoint: String + var headers: [String: String] +} + +struct InitArgs: Codable { + var enableLogging: Bool +} + +struct AuthRequest: Codable { + var socket_id: String + var channel_name: String + var valueUrlEncoded: String? + + init(socket_id: String, channel_name: String) { + self.socket_id = socket_id + self.channel_name = channel_name + self.valueUrlEncoded = "socket_id=\(socket_id)&channel_name=\(channel_name)" + } + + private enum CodingKeys: String, CodingKey { + case socket_id, channel_name + } +} + +struct EventStreamResult: Codable { + var connectionStateChange: ConnectionStateChange? + var connectionError: ConnectionError? + var pusherEvent: Event? + init(connectionStateChange: ConnectionStateChange) { + self.connectionStateChange = connectionStateChange + } + + init(connectionError: ConnectionError) { + self.connectionError = connectionError + } + + init(pusherEvent: Event) { + self.pusherEvent = pusherEvent + } +} + +struct ConnectionStateChange: Codable { + var currentState: String + var previousState: String + + init(currentState: String, previousState: String) { + self.currentState = currentState + self.previousState = previousState + } +} + +struct ConnectionError: Codable { + var message: String + var code: String + var exception: String + + init(message: String, code: String, exception: String) { + self.message = message + self.code = code + self.exception = exception + } +} + +struct Event: Codable { + var eventName: String + var channelName: String? + var userId: String?; + var data: String?; +} + +struct ClientEvent: Codable { + var eventName: String + var channelName: String + var data: String?; +} diff --git a/ios/Classes/PusherClientPlugin.h b/ios/Classes/PusherClientPlugin.h new file mode 100644 index 0000000..ca7b0b0 --- /dev/null +++ b/ios/Classes/PusherClientPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface PusherClientPlugin : NSObject +@end diff --git a/ios/Classes/PusherClientPlugin.m b/ios/Classes/PusherClientPlugin.m new file mode 100644 index 0000000..c561541 --- /dev/null +++ b/ios/Classes/PusherClientPlugin.m @@ -0,0 +1,15 @@ +#import "PusherClientPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "pusher_client-Swift.h" +#endif + +@implementation PusherClientPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftPusherClientPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/PusherService.swift b/ios/Classes/PusherService.swift new file mode 100644 index 0000000..9dcce74 --- /dev/null +++ b/ios/Classes/PusherService.swift @@ -0,0 +1,221 @@ +// +// PusherService.swift +// pusher_client +// +// Created by Romario Chinloy on 10/26/20. +// + +import Flutter +import PusherSwiftWithEncryption + +class PusherService: MChannel { + static let CHANNEL_NAME = "com.github.chinloyal/pusher_client" + static let EVENT_STREAM = "com.github.chinloyal/pusher_client_stream" + static let LOG_TAG = "PusherClientPlugin" + static let PRIVATE_PREFIX = "private-" + static let PRIVATE_ENCRYPTED_PREFIX = "private-encrypted-" + static let PRESENCE_PREFIX = "presence-" + + private var _pusherInstance: Pusher! + private var bindedEvents = Dictionary() + + struct Utils { + static var enableLogging = true + static func debugLog(msg: String) { + if(enableLogging) { + debugPrint("D/\(PusherService.LOG_TAG): \(msg)") + } + } + static func errorLog(msg: String) { + if(enableLogging) { + debugPrint("E/\(PusherService.LOG_TAG): \(msg)") + } + } + } + + func register(messenger: FlutterBinaryMessenger) { + let methodChannel = FlutterMethodChannel(name: PusherService.CHANNEL_NAME, binaryMessenger: messenger) + + methodChannel.setMethodCallHandler { (_ call:FlutterMethodCall, result:@escaping FlutterResult) in + switch call.method { + case "init": + self.`init`(call, result: result) + case "connect": + self.connect(result: result) + case "disconnect": + self.disconnect(result: result) + case "getSocketId": + self.getSocketId(result: result) + case "subscribe": + self.subscribe(call, result: result) + case "unsubscribe": + self.unsubscribe(call, result: result) + case "bind": + self.bind(call, result: result) + case "unbind": + self.unbind(call, result: result) + case "trigger": + self.trigger(call, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + let eventChannel = FlutterEventChannel(name: PusherService.EVENT_STREAM, binaryMessenger: messenger) + + eventChannel.setStreamHandler(StreamHandler.default) + } + + func `init`(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + do { + let json = call.arguments as! String + let pusherArgs: PusherArgs = try JSONDecoder().decode(PusherArgs.self, from: json.data(using: .utf8)!) + + Utils.enableLogging = pusherArgs.initArgs.enableLogging + + if(_pusherInstance == nil) { + let pusherOptions = PusherClientOptions( + authMethod: pusherArgs.pusherOptions.auth == nil ? .noMethod : AuthMethod.authRequestBuilder(authRequestBuilder: AuthRequestBuilder(pusherAuth: pusherArgs.pusherOptions.auth!)), + host: pusherArgs.pusherOptions.cluster != nil ? .cluster(pusherArgs.pusherOptions.cluster!) : .host(pusherArgs.pusherOptions.host), + port: pusherArgs.pusherOptions.encrypted ? pusherArgs.pusherOptions.wssPort : pusherArgs.pusherOptions.wsPort, + useTLS: pusherArgs.pusherOptions.encrypted, + activityTimeout: Double(pusherArgs.pusherOptions.activityTimeout) / 1000 + + ) + + _pusherInstance = Pusher(key: pusherArgs.appKey, options: pusherOptions) + _pusherInstance.connection.reconnectAttemptsMax = pusherArgs.pusherOptions.maxReconnectionAttempts + _pusherInstance.connection.maxReconnectGapInSeconds = Double(pusherArgs.pusherOptions.maxReconnectGapInSeconds) + _pusherInstance.connection.pongResponseTimeoutInterval = Double(pusherArgs.pusherOptions.pongTimeout) / 1000 + _pusherInstance.connection.delegate = ConnectionListener.default + Utils.debugLog(msg: "Pusher initialized") + + result(nil) + } + } catch let err { + Utils.errorLog(msg: err.localizedDescription) + result(FlutterError(code: "INIT_ERROR", message: err.localizedDescription, details: err)) + } + + } + + func connect(result:@escaping FlutterResult) { + _pusherInstance.connect() + result(nil) + } + + func disconnect(result:@escaping FlutterResult) { + _pusherInstance.disconnect() + Utils.debugLog(msg: "Disconnect") + result(nil) + } + + func getSocketId(result:@escaping FlutterResult) { + result(_pusherInstance.connection.socketId) + } + + func subscribe(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + let channelMap = call.arguments as! [String: String] + let channelName: String = channelMap["channelName"]! + var channel: PusherChannel + + if(!channelName.starts(with: PusherService.PRESENCE_PREFIX)) { + channel = _pusherInstance.subscribe(channelName) + } else { + channel = _pusherInstance.subscribeToPresenceChannel(channelName: channelName) + for pEvent in Constants.PresenceEvents.allCases { + channel.bind(eventName: pEvent.rawValue, eventCallback: ChannelEventListener.default.onEvent) + } + } + + for pEvent in Constants.Events.allCases { + channel.bind(eventName: pEvent.rawValue, eventCallback: ChannelEventListener.default.onEvent) + } + + Utils.debugLog(msg: "Subscribed: \(channelName)") + result(nil) + } + + func unsubscribe(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + let channelMap = call.arguments as! [String: String] + let channelName: String = channelMap["channelName"]! + + _pusherInstance.unsubscribe(channelName) + Utils.debugLog(msg: "Unsubscribed: \(channelName)") + + result(nil) + } + + /** + * Note binding can happen before the channel has been subscribed to + */ + func bind(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + let map = call.arguments as! [String: String] + let channelName: String = map["channelName"]! + let eventName: String = map["eventName"]! + var channel: PusherChannel + + if(!channelName.starts(with: PusherService.PRESENCE_PREFIX)) { + channel = _pusherInstance.connection.channels.find(name: channelName)! + bindedEvents[channelName + eventName] = channel.bind(eventName: eventName, eventCallback: ChannelEventListener.default.onEvent) + } else { + channel = _pusherInstance.connection.channels.findPresence(name: channelName)! + bindedEvents[channelName + eventName] = channel.bind(eventName: eventName, eventCallback: ChannelEventListener.default.onEvent) + } + + Utils.debugLog(msg: "[BIND] \(eventName)") + result(nil) + } + + func unbind(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + let map = call.arguments as! [String: String] + let channelName: String = map["channelName"]! + let eventName: String = map["eventName"]! + var channel: PusherChannel + let callBackId = bindedEvents[channelName + eventName] + + if(callBackId != nil) { + if(!channelName.starts(with: PusherService.PRESENCE_PREFIX)) { + channel = _pusherInstance.connection.channels.find(name: channelName)! + channel.unbind(eventName: eventName, callbackId: callBackId!) + } else { + channel = _pusherInstance.connection.channels.findPresence(name: channelName)! + channel.unbind(eventName: eventName, callbackId: callBackId!) + } + } + + Utils.debugLog(msg: "[UNBIND] \(eventName)") + result(nil) + } + + func trigger(_ call:FlutterMethodCall, result:@escaping FlutterResult) { + do { + let json = call.arguments as! String + let clientEvent: ClientEvent = try JSONDecoder().decode(ClientEvent.self, from: json.data(using: .utf8)!) + let channelName = clientEvent.channelName + let eventName = clientEvent.eventName + let data: String = clientEvent.data ?? "" + let errorMessage = "Trigger can only be called on private and presence channels." + + switch clientEvent.channelName { + case _ where channelName.starts(with: PusherService.PRIVATE_ENCRYPTED_PREFIX): + result(FlutterError(code: "TRIGGER_ERROR", message: errorMessage, details: nil)) + case _ where channelName.starts(with: PusherService.PRIVATE_PREFIX): + let channel: PusherChannel = _pusherInstance.connection.channels.find(name: channelName)! + channel.trigger(eventName: eventName, data: data) + case _ where channelName.starts(with: PusherService.PRESENCE_PREFIX): + let channel: PusherPresenceChannel = _pusherInstance.connection.channels.findPresence(name: channelName)! + channel.trigger(eventName: eventName, data: data) + default: + result(FlutterError(code: "TRIGGER_ERROR", message: errorMessage, details: nil)) + } + + Utils.debugLog(msg: "[TRIGGER] \(eventName)") + result(nil) + } catch let err { + Utils.errorLog(msg: err.localizedDescription) + result(FlutterError(code: "TRIGGER_ERROR", message: err.localizedDescription, details: err)) + } + + } +} diff --git a/ios/Classes/StreamHandler.swift b/ios/Classes/StreamHandler.swift new file mode 100644 index 0000000..c6d7987 --- /dev/null +++ b/ios/Classes/StreamHandler.swift @@ -0,0 +1,29 @@ +// +// StreamHandler.swift +// pusher_client +// +// Created by Romario Chinloy on 10/29/20. +// + +class StreamHandler: NSObject, FlutterStreamHandler { + static let `default`: StreamHandler = StreamHandler() + + private override init(){} + + struct Utils { + static var eventSink: FlutterEventSink? + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + Utils.eventSink = events + PusherService.Utils.debugLog(msg: "Event stream listening...") + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + PusherService.Utils.debugLog(msg: "Event stream cancelled.") + return nil + } + + +} diff --git a/ios/Classes/SwiftPusherClientPlugin.swift b/ios/Classes/SwiftPusherClientPlugin.swift new file mode 100644 index 0000000..de7af48 --- /dev/null +++ b/ios/Classes/SwiftPusherClientPlugin.swift @@ -0,0 +1,8 @@ +import Flutter +import UIKit + +public class SwiftPusherClientPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + PusherService().register(messenger: registrar.messenger()) + } +} diff --git a/ios/pusher_client.podspec b/ios/pusher_client.podspec new file mode 100644 index 0000000..7b91941 --- /dev/null +++ b/ios/pusher_client.podspec @@ -0,0 +1,28 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint pusher_client.podspec' to validate before publishing. +# +require 'yaml' +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +libraryVersion = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = 'pusher_client' + s.version = libraryVersion + s.summary = 'A pusher client plugin.' + s.description = <<-DESC +A pusher client plugin that works. + DESC + s.homepage = 'https://github.com/chinloyal/pusher_client' + s.license = { :file => '../LICENSE', :type => 'MIT' } + s.author = { 'Romario Chinloy' => 'jordain7@protonmail.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'PusherSwiftWithEncryption', '~> 8.0.0' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/lib/pusher_client.dart b/lib/pusher_client.dart new file mode 100644 index 0000000..724cd33 --- /dev/null +++ b/lib/pusher_client.dart @@ -0,0 +1,7 @@ +library pusher_client; + +export 'src/pusher/channel.dart'; +export 'src/pusher/pusher_auth.dart'; +export 'src/pusher/pusher_options.dart'; +export 'src/pusher/pusher_client.dart' show PusherClient; +export 'src/pusher/pusher_event.dart'; diff --git a/lib/src/contracts/stream_handler.dart b/lib/src/contracts/stream_handler.dart new file mode 100644 index 0000000..4b51915 --- /dev/null +++ b/lib/src/contracts/stream_handler.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +abstract class StreamHandler { + static const EventChannel _eventStream = const EventChannel('com.github.chinloyal/pusher_client_stream'); + late StreamSubscription _eventStreamSubscription; + + static Map _listeners = {}; + + /// Add a listener to the event channel stream for pusher, + /// any class that extends [StreamHandler] should use this method. + void registerListener(String classId, dynamic Function(dynamic) method) { + StreamHandler._listeners[classId] = method; + _eventStreamSubscription = _eventStream.receiveBroadcastStream().listen(_eventHandler); + } + + /// This method will close the entire event channel stream + /// which is why it should only be used by [PusherClient] + void cancelEventChannelStream() { + _eventStreamSubscription.cancel(); + } + + void _eventHandler(event) { + _listeners.values.forEach((method) { + method(event); + }); + } +} diff --git a/lib/src/models/connection_error.dart b/lib/src/models/connection_error.dart new file mode 100644 index 0000000..2d6303e --- /dev/null +++ b/lib/src/models/connection_error.dart @@ -0,0 +1,19 @@ +part 'connection_error.g.dart'; + +class ConnectionError { + /// A message indicating the cause of the error. + final String? message; + + /// The error code for the message. Can be null. + final String? code; + + /// The exception that was thrown, if any. Can be null. + final String? exception; + + ConnectionError({this.message, this.code, this.exception}); + + factory ConnectionError.fromJson(Map json) => + _$ConnectionErrorFromJson(json); + + Map toJson() => _$ConnectionErrorToJson(this); +} diff --git a/lib/src/models/connection_error.g.dart b/lib/src/models/connection_error.g.dart new file mode 100644 index 0000000..d284ec6 --- /dev/null +++ b/lib/src/models/connection_error.g.dart @@ -0,0 +1,18 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'connection_error.dart'; + +ConnectionError _$ConnectionErrorFromJson(Map json) { + return ConnectionError( + message: json['message'] as String?, + code: json['code'] as String?, + exception: json['exception'] as String?, + ); +} + +Map _$ConnectionErrorToJson(ConnectionError instance) => + { + 'message': instance.message, + 'code': instance.code, + 'exception': instance.exception, + }; diff --git a/lib/src/models/connection_state_change.dart b/lib/src/models/connection_state_change.dart new file mode 100644 index 0000000..5a395a5 --- /dev/null +++ b/lib/src/models/connection_state_change.dart @@ -0,0 +1,20 @@ +part 'connection_state_change.g.dart'; + +class ConnectionStateChange { + /// The current connection state. The state the connection has transitioned + /// to. + final String? currentState; + + /// The previous connections state. The state the connection has transitioned + /// from. + final String? previousState; + + ConnectionStateChange({ + this.currentState, + this.previousState, + }); + + factory ConnectionStateChange.fromJson(Map json) => + _$ConnectionStateChangeFromJson(json); + Map toJson() => _$ConnectionStateChangeToJson(this); +} diff --git a/lib/src/models/connection_state_change.g.dart b/lib/src/models/connection_state_change.g.dart new file mode 100644 index 0000000..7325ea4 --- /dev/null +++ b/lib/src/models/connection_state_change.g.dart @@ -0,0 +1,18 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'connection_state_change.dart'; + +ConnectionStateChange _$ConnectionStateChangeFromJson( + Map json) { + return ConnectionStateChange( + currentState: json['currentState'] as String?, + previousState: json['previousState'] as String?, + ); +} + +Map _$ConnectionStateChangeToJson( + ConnectionStateChange instance) => + { + 'currentState': instance.currentState, + 'previousState': instance.previousState, + }; diff --git a/lib/src/models/event_stream_result.dart b/lib/src/models/event_stream_result.dart new file mode 100644 index 0000000..6d45911 --- /dev/null +++ b/lib/src/models/event_stream_result.dart @@ -0,0 +1,25 @@ +import 'package:pusher_client/src/models/connection_error.dart'; +import 'package:pusher_client/src/models/connection_state_change.dart'; +import 'package:pusher_client/src/pusher/pusher_event.dart'; + +part 'event_stream_result.g.dart'; + +class EventStreamResult { + final ConnectionStateChange? connectionStateChange; + final ConnectionError? connectionError; + final PusherEvent? pusherEvent; + + EventStreamResult({ + this.connectionStateChange, + this.connectionError, + this.pusherEvent, + }); + + bool get isConnectionStateChange => connectionStateChange != null; + bool get isConnectionError => connectionError != null; + bool get isPusherEvent => pusherEvent != null; + + factory EventStreamResult.fromJson(Map json) => + _$EventStreamResultFromJson(json); + Map toJson() => _$EventStreamResultToJson(this); +} diff --git a/lib/src/models/event_stream_result.g.dart b/lib/src/models/event_stream_result.g.dart new file mode 100644 index 0000000..196813d --- /dev/null +++ b/lib/src/models/event_stream_result.g.dart @@ -0,0 +1,26 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'event_stream_result.dart'; + +EventStreamResult _$EventStreamResultFromJson(Map json) { + return EventStreamResult( + connectionStateChange: json['connectionStateChange'] == null + ? null + : ConnectionStateChange.fromJson( + json['connectionStateChange'] as Map), + connectionError: json['connectionError'] == null + ? null + : ConnectionError.fromJson( + json['connectionError'] as Map), + pusherEvent: json['pusherEvent'] == null + ? null + : PusherEvent.fromJson(json['pusherEvent'] as Map), + ); +} + +Map _$EventStreamResultToJson(EventStreamResult instance) => + { + 'connectionStateChange': instance.connectionStateChange, + 'connectionError': instance.connectionError, + 'pusherEvent': instance.pusherEvent, + }; diff --git a/lib/src/pusher/channel.dart b/lib/src/pusher/channel.dart new file mode 100644 index 0000000..9bf4df9 --- /dev/null +++ b/lib/src/pusher/channel.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:pusher_client/src/contracts/stream_handler.dart'; +import 'package:pusher_client/src/models/event_stream_result.dart'; +import 'package:pusher_client/src/pusher/pusher_event.dart'; + +class Channel extends StreamHandler { + static const MethodChannel _mChannel = + const MethodChannel('com.github.chinloyal/pusher_client'); + static const classId = 'Channel'; + + static Map _eventCallbacks = + Map(); + + final String name; + + Channel(this.name) { + _subscribe(); + } + + void _subscribe() { + _mChannel.invokeMethod('subscribe', { + 'channelName': name, + }); + } + + /// Binds a callback ([onEvent]) to an event. The + /// callback will be notified whenever the specified + /// [eventName] is received on this channel. + Future bind( + String eventName, + void Function(PusherEvent? event) onEvent, + ) async { + registerListener(classId, _eventHandler); + _eventCallbacks[this.name + eventName] = onEvent; + + await _mChannel.invokeMethod('bind', { + 'channelName': this.name, + 'eventName': eventName, + }); + } + + /// Unbinds the callback from the given [eventName], in the scope + /// of the channel being acted upon + Future unbind(String eventName) async { + _eventCallbacks.remove(this.name + eventName); + + await _mChannel.invokeMethod('unbind', { + 'channelName': this.name, + 'eventName': eventName, + }); + } + + /// Once subscribed it is possible to trigger client events on a private + /// channel as long as client events have been activated for the a Pusher + /// application. There are a number of restrictions enforced with client + /// events. For full details see the + /// [documentation](http://pusher.com/docs/client_events) + /// + /// The [eventName] to trigger must have a `client-` prefix. + Future trigger(String eventName, dynamic data) async { + if (!eventName.startsWith('client-')) { + eventName = "client-$eventName"; + } + + await _mChannel.invokeMethod( + 'trigger', + jsonEncode({ + 'eventName': eventName, + 'data': data.toString(), + 'channelName': this.name, + }), + ); + } + + Future _eventHandler(event) async { + var result = EventStreamResult.fromJson(jsonDecode(event.toString())); + + if (result.isPusherEvent) { + var callback = _eventCallbacks[ + result.pusherEvent!.channelName! + result.pusherEvent!.eventName!]; + if (callback != null) { + callback(result.pusherEvent); + } + } + } +} diff --git a/lib/src/pusher/pusher_auth.dart b/lib/src/pusher/pusher_auth.dart new file mode 100644 index 0000000..308f879 --- /dev/null +++ b/lib/src/pusher/pusher_auth.dart @@ -0,0 +1,18 @@ +part 'pusher_auth.g.dart'; + +class PusherAuth { + /// The endpoint to be called when authenticating. + final String? endpoint; + + /// Additional headers to be sent as part of the request. + final Map? headers; + + PusherAuth( + this.endpoint, { + this.headers = const {'Content-Type': 'application/json'}, + }); + + factory PusherAuth.fromJson(Map json) => + _$PusherAuthFromJson(json); + Map toJson() => _$PusherAuthToJson(this); +} diff --git a/lib/src/pusher/pusher_auth.g.dart b/lib/src/pusher/pusher_auth.g.dart new file mode 100644 index 0000000..64d20b7 --- /dev/null +++ b/lib/src/pusher/pusher_auth.g.dart @@ -0,0 +1,18 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'pusher_auth.dart'; + +PusherAuth _$PusherAuthFromJson(Map json) { + return PusherAuth( + json['endpoint'] as String?, + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + ); +} + +Map _$PusherAuthToJson(PusherAuth instance) => + { + 'endpoint': instance.endpoint, + 'headers': instance.headers, + }; diff --git a/lib/src/pusher/pusher_client.dart b/lib/src/pusher/pusher_client.dart new file mode 100644 index 0000000..4394608 --- /dev/null +++ b/lib/src/pusher/pusher_client.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:pusher_client/pusher_client.dart'; +import 'package:pusher_client/src/contracts/stream_handler.dart'; +import 'package:pusher_client/src/models/connection_error.dart'; +import 'package:pusher_client/src/models/connection_state_change.dart'; +import 'package:pusher_client/src/models/event_stream_result.dart'; +import 'package:pusher_client/src/pusher/channel.dart'; + +part 'pusher_client.g.dart'; + +/// This class is the main entry point for accessing Pusher. +/// By creating a new [PusherClient] instance a connection is +/// created automatically unless [autoConnect] is set to false, +/// if auto connect is disabled this means you can call +/// `connect()` at a later point. +class PusherClient extends StreamHandler { + static const MethodChannel _channel = + const MethodChannel('com.github.chinloyal/pusher_client'); + static const classId = 'PusherClient'; + + static PusherClient? _singleton; + void Function(ConnectionStateChange?)? _onConnectionStateChange; + void Function(ConnectionError?)? _onConnectionError; + String? _socketId; + + PusherClient._( + String appKey, + PusherOptions options, { + bool enableLogging = true, + bool autoConnect = true, + }); + + /// Creates a [PusherClient] -- returns the instance if it's already be called. + factory PusherClient( + String appKey, + PusherOptions options, { + bool enableLogging = true, + bool autoConnect = true, + }) { + _singleton ??= PusherClient._( + appKey, + options, + enableLogging: enableLogging, + autoConnect: autoConnect, + ); + + final initArgs = InitArgs(enableLogging: enableLogging); + + _singleton!._init(appKey, options, initArgs); + + if (autoConnect) _singleton!.connect(); + + return _singleton!; + } + + Future _init(String appKey, PusherOptions options, InitArgs initArgs) async { + registerListener(classId, _eventHandler); + await _channel.invokeMethod( + 'init', + jsonEncode({ + 'appKey': appKey, + 'pusherOptions': options, + 'initArgs': initArgs, + }), + ); + } + + /// Subscribes the client to a new channel + /// + /// Note that subscriptions should be registered only once with a Pusher + /// instance. Subscriptions are persisted over disconnection and + /// re-registered with the server automatically on reconnection. This means + /// that subscriptions may also be registered before `connect()` is called, + /// they will be initiated on connection. + Channel subscribe(String channelName) { + return Channel(channelName); + } + + /// Unsubscribes from a channel using the name of the channel. + Future unsubscribe(String channelName) async { + await _channel.invokeMethod('unsubscribe', { + 'channelName': channelName, + }); + } + + /// Initiates a connection attempt using the client's + /// existing connection details + Future connect() async { + await _channel.invokeMethod('connect'); + } + + /// Disconnects the client's connection + Future disconnect() async { + await _channel.invokeMethod('disconnect'); + + cancelEventChannelStream(); + } + + /// The id of the current connection + String? getSocketId() => _socketId; + + /// Callback that is fired whenever the connection state of the + /// connection changes. The state typically changes during connection + /// to Pusher and during disconnection and reconnection. + void onConnectionStateChange( + void Function(ConnectionStateChange? state) callback) { + _onConnectionStateChange = callback; + } + + /// Callback that indicates either: + /// - An error message has been received from Pusher, or + /// - An error has occurred in the client library. + void onConnectionError(void Function(ConnectionError? error) callback) { + _onConnectionError = callback; + } + + Future _eventHandler(event) async { + var result = EventStreamResult.fromJson(jsonDecode(event.toString())); + + if (result.isConnectionStateChange) { + _socketId = await _channel.invokeMethod('getSocketId'); + + if (_onConnectionStateChange != null) + _onConnectionStateChange!(result.connectionStateChange); + } + + if (result.isConnectionError) { + if (_onConnectionError != null) + _onConnectionError!(result.connectionError); + } + } +} + +class InitArgs { + final bool? enableLogging; + + InitArgs({ + this.enableLogging, + }); + + Map toJson() => _$InitArgsToJson(this); +} diff --git a/lib/src/pusher/pusher_client.g.dart b/lib/src/pusher/pusher_client.g.dart new file mode 100644 index 0000000..c7228e8 --- /dev/null +++ b/lib/src/pusher/pusher_client.g.dart @@ -0,0 +1,13 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'pusher_client.dart'; + +// InitArgs _$InitArgsFromJson(Map json) { +// return InitArgs( +// enableLogging: json['enableLogging'] as bool, +// ); +// } + +Map _$InitArgsToJson(InitArgs instance) => { + 'enableLogging': instance.enableLogging, + }; diff --git a/lib/src/pusher/pusher_event.dart b/lib/src/pusher/pusher_event.dart new file mode 100644 index 0000000..2f33bc7 --- /dev/null +++ b/lib/src/pusher/pusher_event.dart @@ -0,0 +1,29 @@ +part 'pusher_event.g.dart'; + +class PusherEvent { + /// The name of the channel that the event was triggered on. Not present + /// in events without an associated channel, For example "pusher:error" events + /// relating to the connection. + final String? channelName; + + /// The name of the event. + final String? eventName; + + /// The data that was passed when the event was triggered. + final String? data; + + /// The ID of the user who triggered the event. Only present in + /// client event on presence channels. + final String? userId; + + PusherEvent({ + this.channelName, + this.eventName, + this.data, + this.userId, + }); + + factory PusherEvent.fromJson(Map json) => + _$PusherEventFromJson(json); + Map toJson() => _$PusherEventToJson(this); +} diff --git a/lib/src/pusher/pusher_event.g.dart b/lib/src/pusher/pusher_event.g.dart new file mode 100644 index 0000000..4032c8f --- /dev/null +++ b/lib/src/pusher/pusher_event.g.dart @@ -0,0 +1,20 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'pusher_event.dart'; + +PusherEvent _$PusherEventFromJson(Map json) { + return PusherEvent( + channelName: json['channelName'] as String?, + eventName: json['eventName'] as String?, + data: json['data'] as String?, + userId: json['userId'] as String?, + ); +} + +Map _$PusherEventToJson(PusherEvent instance) => + { + 'channelName': instance.channelName, + 'eventName': instance.eventName, + 'data': instance.data, + 'userId': instance.userId, + }; diff --git a/lib/src/pusher/pusher_options.dart b/lib/src/pusher/pusher_options.dart new file mode 100644 index 0000000..81dc222 --- /dev/null +++ b/lib/src/pusher/pusher_options.dart @@ -0,0 +1,63 @@ +import 'package:pusher_client/src/pusher/pusher_auth.dart'; + +part 'pusher_options.g.dart'; + +class PusherOptions { + /// Sets the authorization options to be used when authenticating private, + /// private-encrypted and presence channels. + final PusherAuth? auth; + + /// A "cluster" represents the physical location of the servers that handle + /// requests from your Channels app. + /// + /// For example, the Channels cluster `mt1` is in Northern Virginia + /// in the United States. + final String? cluster; + + /// The host to which connections will be made. + final String? host; + + /// Whether the connection to Pusher should be use TLS. + final bool? encrypted; + + /// The number of milliseconds of inactivity at which a "ping" will be + /// triggered to check the connection. + final int? activityTimeout; + + /// The port to which non TLS connections will be made. + final int? wsPort; + + /// The port to which encrypted connections will be made. + final int? wssPort; + + /// The number of milliseconds after a "ping" is sent that the client will + /// wait to receive a "pong" response from the server before considering the + /// connection broken and triggering a transition to the disconnected state. + /// + /// The default value is 30,000. + final int? pongTimeout; + + /// Number of reconnect attempts when websocket connection failed + final int? maxReconnectionAttempts; + + /// The delay in two reconnection extends exponentially (1, 2, 4, .. seconds) + /// This property sets the maximum in between two reconnection attempts. + final int? maxReconnectGapInSeconds; + + PusherOptions({ + this.auth, + this.cluster, + this.host = "ws.pusherapp.com", + this.wsPort = 80, + this.wssPort = 443, + this.encrypted = true, + this.activityTimeout = 120000, + this.pongTimeout = 30000, + this.maxReconnectionAttempts = 6, + this.maxReconnectGapInSeconds = 30, + }); + + factory PusherOptions.fromJson(Map json) => + _$PusherOptionsFromJson(json); + Map toJson() => _$PusherOptionsToJson(this); +} diff --git a/lib/src/pusher/pusher_options.g.dart b/lib/src/pusher/pusher_options.g.dart new file mode 100644 index 0000000..8f93712 --- /dev/null +++ b/lib/src/pusher/pusher_options.g.dart @@ -0,0 +1,34 @@ +// YOU SHOULD MODIFY BY HAND + +part of 'pusher_options.dart'; + +PusherOptions _$PusherOptionsFromJson(Map json) { + return PusherOptions( + auth: json['auth'] == null + ? null + : PusherAuth.fromJson(json['auth'] as Map), + cluster: json['cluster'] as String?, + host: json['host'] as String?, + wsPort: json['wsPort'] as int?, + wssPort: json['wssPort'] as int?, + encrypted: json['encrypted'] as bool?, + activityTimeout: json['activityTimeout'] as int?, + pongTimeout: json['pongTimeout'] as int?, + maxReconnectionAttempts: json['maxReconnectionAttempts'] as int?, + maxReconnectGapInSeconds: json['maxReconnectGapInSeconds'] as int?, + ); +} + +Map _$PusherOptionsToJson(PusherOptions instance) => + { + 'auth': instance.auth, + 'cluster': instance.cluster, + 'host': instance.host, + 'encrypted': instance.encrypted, + 'activityTimeout': instance.activityTimeout, + 'pongTimeout': instance.pongTimeout, + 'wsPort': instance.wsPort, + 'wssPort': instance.wssPort, + 'maxReconnectionAttempts': instance.maxReconnectionAttempts, + 'maxReconnectGapInSeconds': instance.maxReconnectGapInSeconds, + }; diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..52bae02 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,29 @@ +name: pusher_client +description: | + A Pusher Channels client plugin for Flutter targeting Android and iOS. It + wraps pusher-websocket-java v2.2.5 and pusher-websocket-swift v8.0.0. +version: 2.0.0 +homepage: https://github.com/chinloyal/pusher_client +documentation: https://github.com/chinloyal/pusher_client/blob/master/README.md + + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.20.0 <2.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: com.github.chinloyal.pusher_client + pluginClass: PusherClientPlugin + ios: + pluginClass: PusherClientPlugin diff --git a/pusher_client.iml b/pusher_client.iml new file mode 100644 index 0000000..73e7ebd --- /dev/null +++ b/pusher_client.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/pusher_client_test.dart b/test/pusher_client_test.dart new file mode 100644 index 0000000..a959c84 --- /dev/null +++ b/test/pusher_client_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pusher_client/pusher_client.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + MethodChannel channel = + const MethodChannel('com.github.chinloyal/pusher_client'); + group('PusherClient Test | ', () { + setUp(() { + channel.setMockMethodCallHandler((call) { + switch (call.method) { + case 'init': + return null; + case 'connect': + return null; + default: + throw UnimplementedError(); + } + }); + }); + + test('Pusher client returns a singleton', () { + var pusher1 = PusherClient('key', PusherOptions()); + var pusher2 = PusherClient('key', PusherOptions()); + + expect(pusher1.hashCode, pusher2.hashCode); + }); + + // test() + }); +}