From 45f3087ebcd64229fe57c191c877d9ebf5e97112 Mon Sep 17 00:00:00 2001 From: mahdi-malv Date: Tue, 1 Dec 2020 09:25:21 +0000 Subject: [PATCH] Changes of version 2.4.0 --- .../-------------------------.md | 27 + .github/ISSUE_TEMPLATE/-------------------.md | 38 + .github/ISSUE_TEMPLATE/----------------.md | 37 + .github/workflows/main.yml | 42 ++ .gitignore | 13 + .metadata | 10 + CHANGELOG.md | 143 ++++ LICENSE | 13 + README.md | 30 + android/.gitignore | 8 + android/build.gradle | 53 ++ android/gradle.properties | 3 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + android/gradlew | 172 +++++ android/gradlew.bat | 84 +++ android/settings.gradle | 1 + android/src/main/AndroidManifest.xml | 3 + .../kotlin/co/pushe/plus/flutter/Constants.kt | 11 + .../co/pushe/plus/flutter/HandleStorage.kt | 55 ++ .../co/pushe/plus/flutter/LatchResult.kt | 31 + .../co/pushe/plus/flutter/PusheChandler.kt | 627 ++++++++++++++++ .../plus/flutter/PusheFlutterApplication.java | 31 + .../pushe/plus/flutter/PusheFlutterPlugin.kt | 123 ++++ .../flutter/PusheInAppMessagingListener.kt | 68 ++ .../plus/flutter/PusheNotificationListener.kt | 319 +++++++++ .../kotlin/co/pushe/plus/flutter/Utils.kt | 232 ++++++ example/.gitignore | 70 ++ example/README.md | 8 + example/android/app/build.gradle | 67 ++ example/android/app/multidex-config.pro | 4 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 52 ++ .../EmbeddingV1Activity.java | 13 + .../pushesampleflutter/MainActivity.java | 8 + .../co/pushe/pushesampleflutter/MyApp.java | 30 + .../pushesampleflutter/MyFCMService.java | 37 + .../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 | 8 + .../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 | 15 + example/ios/Flutter/AppFrameworkInfo.plist | 26 + example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Runner.xcodeproj/project.pbxproj | 514 ++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 93 +++ .../contents.xcworkspacedata | 7 + example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 11112 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 | 45 ++ example/ios/Runner/Runner-Bridging-Header.h | 1 + example/lib/main.dart | 18 + example/lib/pushe_sample.dart | 466 ++++++++++++ example/lib/utils.dart | 84 +++ example/pubspec.lock | 161 +++++ example/pubspec.yaml | 64 ++ example/test/widget_test.dart | 27 + ios/.gitignore | 37 + ios/Assets/.gitkeep | 0 ios/Classes/PusheFlutterPlugin.h | 4 + ios/Classes/PusheFlutterPlugin.m | 15 + ios/Classes/SwiftPusheFlutterPlugin.swift | 92 +++ ios/pushe_flutter.podspec | 26 + lib/pushe.dart | 668 ++++++++++++++++++ pubspec.lock | 147 ++++ pubspec.yaml | 26 + test/pushe_flutter_test.dart | 21 + 98 files changed, 5336 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/-------------------------.md create mode 100644 .github/ISSUE_TEMPLATE/-------------------.md create mode 100644 .github/ISSUE_TEMPLATE/----------------.md create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .metadata 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.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/Constants.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/HandleStorage.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/LatchResult.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/PusheChandler.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterApplication.java create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterPlugin.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/PusheInAppMessagingListener.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/PusheNotificationListener.kt create mode 100644 android/src/main/kotlin/co/pushe/plus/flutter/Utils.kt create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/multidex-config.pro 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/java/co/pushe/pushesampleflutter/EmbeddingV1Activity.java create mode 100644 example/android/app/src/main/java/co/pushe/pushesampleflutter/MainActivity.java create mode 100644 example/android/app/src/main/java/co/pushe/pushesampleflutter/MyApp.java create mode 100644 example/android/app/src/main/java/co/pushe/pushesampleflutter/MyFCMService.java 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/Flutter/AppFrameworkInfo.plist create mode 100644 example/ios/Flutter/Debug.xcconfig create mode 100644 example/ios/Flutter/Release.xcconfig 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/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/ios/Runner.xcworkspace/contents.xcworkspacedata 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/lib/pushe_sample.dart create mode 100644 example/lib/utils.dart create mode 100644 example/pubspec.lock 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/PusheFlutterPlugin.h create mode 100644 ios/Classes/PusheFlutterPlugin.m create mode 100644 ios/Classes/SwiftPusheFlutterPlugin.swift create mode 100644 ios/pushe_flutter.podspec create mode 100644 lib/pushe.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/pushe_flutter_test.dart diff --git a/.github/ISSUE_TEMPLATE/-------------------------.md b/.github/ISSUE_TEMPLATE/-------------------------.md new file mode 100644 index 0000000..78430f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-------------------------.md @@ -0,0 +1,27 @@ +--- +name: "\U0001F680 اضافه‌کردن قابلیت جدید" +about: در صورتی که درخواست دارید که قابلیتی جدید به لایبرری اضافه شود +title: "\U0001F680 [FEATURE]: " +labels: enhancement +assignees: '' + +--- + +
+ +[//]: # ([FEATURE] و اموجی در تیتر برای وضوح مشکل ترجیحا باقی بماند) +[//]: # (لطفا حتما تمپلیت را رعایت کنید تا مشکل به خوبی توضیح داده شود و متون تمپلیت را پاک نکنید) + + +**آیا این قابلیت مرتبط با مشکلی است؟ توضیح دهید** +در صورتی که این قابلیت برگرفته از یک مشکل است لطفا مشکل را شرح‌دهید + +[//]: # (در صورتی که قصد دارید مشکلی را بیان کنید و درخواست قابلیتی را ندارید بایستی مشکل را بصورت خطا یا باگ مطرح کنید و نه قابلیت) + +**شرح قابلیت و کارکرد آن** +این قابلیت چه استفاده‌ای دارد و چه مشکلی را حل می‌کند یا چه امکانی را اضافه می‌کند + +**در حال حاضر جایگزینی برای این قابلیت وجود دارد؟** +در صورتی که این قابلیت نباشد آیا راه جایگزینی وجود دارد؟ + +
diff --git a/.github/ISSUE_TEMPLATE/-------------------.md b/.github/ISSUE_TEMPLATE/-------------------.md new file mode 100644 index 0000000..247c2d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-------------------.md @@ -0,0 +1,38 @@ +--- +name: "❓: بیان سوال یا خطا" +about: در صورتی که اروری در هر موردی رخ‌داده که در سوالات و خطاهای مستندات ذکر نشده‌است +title: "❓ [ERROR]: " +labels: question +assignees: '' + +--- + +
+ +[//]: # ([ERROR] و اموجی در تیتر برای وضوح مشکل ترجیحا باقی بماند) +[//]: # (لطفا حتما تمپلیت را رعایت کنید تا مشکل به خوبی توضیح داده شود و متون تمپلیت را پاک نکنید) + +**شرح خطا** +خطایی که رخ‌داده است را شرح‌دهید + +[//]: # (در صورتی که نصب شما ثبت نمی‌شود لطفا خطاهای مستندات را مطالعه کنید) +[//]: # (برای اضافه‌کردن کد آن را از بلاک div خارج کنید تا سمت راست قرار نگیرد) + +**لاگ خطا** +لاگی که در لاگ‌کت هنگام رخ‌دادن خطا چاپ می‌شود. متن کامل استک‌تریس را چاپ کنید. + +
+ +``` +paste log here +``` + +
+ +**اطلاعات محیط تست** +- نسخه‌ی پوشه یا پلاگین +- نسخه‌ی فریم‌ورک +- سیستم‌عامل (Android/iOS) +- TargetSDK/CompileSDK (Android) + +
diff --git a/.github/ISSUE_TEMPLATE/----------------.md b/.github/ISSUE_TEMPLATE/----------------.md new file mode 100644 index 0000000..408c9f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/----------------.md @@ -0,0 +1,37 @@ +--- +name: "\U0001F41B باگ در پلاگین" +about: وجود باگ یا مشکل در پلاگین +title: "\U0001F41B [BUG]: " +labels: bug +assignees: '' + +--- + +
+ +[//]: # ([BUG] و اموجی در تیتر برای وضوح مشکل ترجیحا باقی بماند) +[//]: # (لطفا حتما تمپلیت را رعایت کنید تا مشکل به خوبی توضیح داده شود و متون تمپلیت را پاک نکنید) + +**شرح باگ** +باگی که با آن مواجه شدید را شرح دهید + +[//]: # (در صورتی که نصب ثبت نمی‌شود لطفا به مستندات مراجعه کنید و خطاها را مطالعه نمایید) +[//]: # (برای اضافه‌کردن کد آن‌را خارج از بلاک div قرار دهید تا سمت راست قرار نگیرد) + +**تولید مجدد باگ** +گام‌های لازم را که برای تولید باگ بایستی طی کرد را لیست کنید + + +**اطلاعات محیط تست** +- سیستم‌عامل (iOS/Android) +- نسخه‌ی پوشه یا پلاگین پوشه +- CompileSDK/TargetSDK (For Android) +- اطلاعات دستگاهی که با آن تست کردید + +**لاگ خطا در لاگ‌کت** +لاگی که در لاگ کت وجود دارد (استک‌تریس کامل) را وارد کنید + +**اطلاعات اضافی** +اطلاعاتی که می‌تواند به وضوح بیان مشکل کمک کند + +
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..a1fc228 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,42 @@ +name: Deploy + +on: + push: + branches: + - master + +jobs: + deploy-sample: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup git + run: | + echo "Initializing git" + git config --global user.name "Mahdi-Malv" + git config --global user.email "mmalvandi75@gmail.com" + - uses: webfactory/ssh-agent@v0.4.1 + with: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + ssh-private-key: ${{ secrets.ACTION_PRIVATE_KEY }} + - name: Chaning local version to global version in sample + run: | + VERSION=$(cat pubspec.yaml | grep -i 'version:' | awk '{ print $2 }') + cd example + sed -i -e "s@path: ../@ @g" pubspec.yaml + var3='pushe_flutter:'; var4="pushe_flutter: $VERSION" + sed -i -e "s@$var3@$var4@g" pubspec.yaml + cd .. + - name: Push sample + run: | + VERSION=$(cat pubspec.yaml | grep -i 'version:' | awk '{ print $2 }') + rm -rf .git + cd example + git init + git remote add example git@github.com:pusheco/pushe-flutter-sample.git + git add -A + git commit -m "Update example with pushe:$VERSION" + git push -f example master + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80091ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ + +*.iml + +.idea/ + +example/.metadata diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..9e63b03 --- /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: f30b7f4db93ee747cd727df747941a28ead25ff5 + channel: stable + +project_type: plugin diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0f11b62 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,143 @@ +# ChangeLog +## 2.4.0 + +> **New** +> - PusheFlutter now features iOS support +> - Android supports Huawei push notifications using `hms` module + +### Android +- Update native library to `2.4.1-beta05` which includes: + - `hms` module adding support for sending push notifications on Huawei devices (HMSCore) + - Improvements and bug fixes + +### iOS +- Stable changes of `2.3.0-alpha01` + +## 2.3.0-alpha01 + +> **New** +> PusheFlutter now features iOS support + +- [change] Inner plugin classes are changed to respect iOS native classes +- **Android**: Update native library to `2.2.1` + + +## 2.2.0 + +- Introducing **InAppMessaging** module added to plugin +- Added APIs: + * `triggerEvent` for triggering local events. + * `disableInAppMessaging`/`EnableInAppMessaging`/`isInAppMessagingEnabled` to control whether message should be shown or not. + * `setInAppMessagingListener` to get a callback when a specific event occurred on InAppMessaging module + * `dismissShownInApp` to remove shown InApp message using code + * `testInAppMessage` for testing purposes using code +- **Fix**: Bug in `sendNotificationToUser` when type was `DeviceId` +- **Fix**: PlatformChannel crash after successful `sendNotificationToUser` + +## 2.1.1 +- Update Native Android dependency to `pushe:2.1.1` +- Custom RxJava is used to avoid large size when not needed + - If developer or any library is using RxJava, `duplicate` error might be thrown + In that case you should exclude RxJava and instead implement a normal version +- Added support for GDPR compliance +- Native library has been migrated to AndroidX +- Deprecate `getAndroidId`. Instead, `getDeviceId` should be used + +## 2.1.1-alpha01 +- Update native dependency to `pushe:2.1.1-beta08` + +## 2.1.0 + +- Added Support for **Flutter Embedding V2** +- Migrate native language to Kotlin +- Added APIs: + * `createNotificationChannel` + * `removeNotificationChannel` + * `enableCustomSound` + * `DisableCustomSound` +- Improvements on analytics methods `sendEvent` and `sendEcommerceData` +- Added support for background execution to get the callbacks event when the app is fully killed +- Fix bug when clearing `customId`, `userEmail` and `userPhoneNumber`. You can now set null to clear them. + +## 2.0.3 + +- Fix bug in notification listeners +- Improve `sendNotificationToUser` to support multiple IDs +- Function callbacks will have no boolean status anymore, since there was no false status +- Code style improvements +- Example project improvements + +## 2.0.2 + +- Fix issue with AndroidX + +## 2.0.1 + +- Fix formatting of plugin +- Minor improvements + +## 2.0.0 + +* Migrate to the new Plus sdk of Pushe +* Get used of new Plus features in the SDK +* No initialization is needed for the library +> Notice the `setNotificationListener` is not fully reliable yet, since it does not handle background + +## 1.1.0-alpha1 + +* Fixed Battery usage issue +* Added method `isNotificationOn` + +## 1.0.1 + +* Fix problem with **AndroidX** projects. + +* Changed example package name. + +## 1.0.0 + +* Release ready version. + +* New listener API for notification callbacks. + +* Removed extra files and APIs. + +* Remove extra Fcm service. Firebase and other services can now be added and supported natively. + +* Minor improvements and bug fixes. + +## 0.9.1 + +* Recreating notification callbacks. Callbacks will return actual notification objects now. + +* From now on, Plugin can be used along with Firebase messaging plugin. + +* Minor improvements and bug fixes. + +## 0.2.1 + +* Added better styled callbacks. + +* Minor improvements. + +## 0.0.2 + +* Bug fixed on notification listeners not getting called. + +* Fixed a little bug in example app. + +* Listeners of notification callbacks are working. + +* Added Release offline AAR package. + +* More comments in plugin. + +## 0.0.1 + +* Pushe basic commands. + +* Support for Android OS. + +* Notification content callback. + +**Note**: Callbacks will be passed when flutter is running. So when the app is closed, notifications will not call the callback methods (They actually will, but the flutter doesn't get it). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..86c0662 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2019 Pusheco + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9134e9 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Pushe flutter + +**Pushe** notification service official plugin for Flutter. Pushe is a push notification service. Refer to [Pushe Homepage](https://pushe.co) for more information. +It supports **Android** and **iOS** (it's also possible to use it on [Flutter Web](https://medium.com/@malv/add-pushe-web-push-to-a-flutter-website-a9b1ab736e57)) + +## Installation + +Add the plugin to `pubspec.yaml`: + +version: [![pub package](https://img.shields.io/pub/v/pushe_flutter)](https://pub.dartlang.org/packages/pushe_flutter) + +```yaml +dependencies: + pushe_flutter: +``` + +* If you want to use the latest version, not necessarily released and stable, you can directly use the source code on Github. + +```yaml +pushe_flutter: + git: + url: https://github.com/pusheco/pushe-flutter.git +``` + +Visit the [**Documentation**](https://docs.pushe.co/docs/flutter/intro/) for more information about usage and API reference. + +## More Info + +* FAQ and issues in [Github repo](https://github.com/pusheco/pushe-flutter/issues?q=is%3Aissue+) +* Sample project is in the library source code and in the [Sample repo on github](https://github.com/pusheco/pushe-flutter-sample) 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..6910e3e --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,53 @@ +group 'co.pushe.plus.flutter' +version '1.0-SNAPSHOT' +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +buildscript { + ext.kotlin_version = '1.3.72' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.0.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + maven { url "https://dl.bintray.com/pushe/preview" } + maven {url 'https://developer.huawei.com/repo/'} + } +} + + + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + implementation ("co.pushe.plus:base:2.4.1-beta05") + api "androidx.multidex:multidex:2.0.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..d12b9a8 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..39cb497 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 19 10:37:37 IRST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..6f9fd69 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pushe' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..64255fa --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/Constants.kt b/android/src/main/kotlin/co/pushe/plus/flutter/Constants.kt new file mode 100644 index 0000000..0307cc6 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/Constants.kt @@ -0,0 +1,11 @@ +package co.pushe.plus.flutter + +object Constants { + + // Types + const val RECEIVE = "receive" + const val CLICK = "click" + const val DISMISS = "dismiss" + const val CUSTOM_CONTENT = "custom_content" + const val BUTTON_CLICK = "button_click" +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/HandleStorage.kt b/android/src/main/kotlin/co/pushe/plus/flutter/HandleStorage.kt new file mode 100644 index 0000000..95e75f0 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/HandleStorage.kt @@ -0,0 +1,55 @@ +package co.pushe.plus.flutter + +import android.content.Context + +/** + * This object is mainly used to save and retrieve callback handles of dart side callback methods. + * One is the setup which will be called when the plugin was initialized on app startup. + * The other one is the callback which developer defines it as a static method or top level function and passes it to Plugin. + * + * SetupHandle: Is a top level method which that handles waking the channel and initializing for telling the plugin that the isolate is running + * and plugin can call the background stuff and send them through channel to dart. + * + * MessageHandle: Is the top level or static method that the user defines and passes to the `Pushe.setNotificationListener`. + */ +internal object HandleStorage { + private const val SHARED_PREFERENCES_KEY = "pushe_storage" + private const val BACKGROUND_SETUP_CALLBACK_HANDLE_KEY = "pushe_background_setup_handle" + private const val BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY = "pushe_background_message_handle" + + @JvmStatic + fun saveSetupHandle(context: Context, setupHandle: Long) { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + preferences.edit().putLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, setupHandle).apply() + } + + @JvmStatic + fun saveMessageHandle(context: Context, messageHandle: Long) { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + preferences.edit().putLong(BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY, messageHandle).apply() + } + + @JvmStatic + fun getSetupHandle(context: Context): Long { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + return preferences.getLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, 0) + } + + @JvmStatic + fun getMessageHandle(context: Context): Long { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + return preferences.getLong(BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY, 0) + } + + @JvmStatic + fun hasSetupHandle(context: Context): Boolean { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + return preferences.contains(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY) + } + + @JvmStatic + fun hasMessageHandle(context: Context): Boolean { + val preferences = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0x0000) + return preferences.contains(BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/LatchResult.kt b/android/src/main/kotlin/co/pushe/plus/flutter/LatchResult.kt new file mode 100644 index 0000000..409c2d2 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/LatchResult.kt @@ -0,0 +1,31 @@ +package co.pushe.plus.flutter + +import co.pushe.plus.flutter.Utils.lg +import io.flutter.plugin.common.MethodChannel +import java.util.concurrent.CountDownLatch + +/** + * A concurrent handler of the Flutter background isolate. + */ +internal class LatchResult(latch: CountDownLatch) { + val result: MethodChannel.Result + + init { + result = object : MethodChannel.Result { + override fun success(result: Any?) { + lg("MethodChannel result, success") + latch.countDown() + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + lg("MethodChannel result, error") + latch.countDown() + } + + override fun notImplemented() { + lg("MethodChannel result, notImplemented") + latch.countDown() + } + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/PusheChandler.kt b/android/src/main/kotlin/co/pushe/plus/flutter/PusheChandler.kt new file mode 100644 index 0000000..22773a9 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheChandler.kt @@ -0,0 +1,627 @@ +package co.pushe.plus.flutter + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import androidx.annotation.VisibleForTesting +import co.pushe.plus.Pushe +import co.pushe.plus.analytics.PusheAnalytics +import co.pushe.plus.analytics.event.Ecommerce +import co.pushe.plus.analytics.event.Event +import co.pushe.plus.analytics.event.EventAction +import co.pushe.plus.flutter.HandleStorage.saveMessageHandle +import co.pushe.plus.flutter.HandleStorage.saveSetupHandle +import co.pushe.plus.flutter.InAppUtils.getInAppMessageAndButtonFromIntent +import co.pushe.plus.flutter.InAppUtils.getInAppMessageFromIntent +import co.pushe.plus.flutter.Pack.getCustomContentFromIntent +import co.pushe.plus.flutter.Pack.getNotificationJsonFromIntent +import co.pushe.plus.flutter.Utils.lg +import co.pushe.plus.inappmessaging.PusheInAppMessaging +import co.pushe.plus.notification.PusheNotification +import co.pushe.plus.notification.UserNotification +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.view.FlutterMain + +/** + * PusheChandler + * + * @author Mahdi Malvandi + */ +@Suppress("SpellCheckingInspection") +internal class PusheChandler(private val context: Context, + private val messenger: BinaryMessenger) : BroadcastReceiver(), MethodCallHandler { + + /** + * Register a receiver to get Pushe foreground callbacks + * See `onReceive` at the end of the file + */ + init { + val i = IntentFilter() + val packageName = context.packageName + i.addAction("$packageName.nr") // Receive + i.addAction("$packageName.nc") // Click + i.addAction("$packageName.nbc") // Button click + i.addAction("$packageName.nd") // Dismiss + i.addAction("$packageName.nccr") // CustomContent receive + + i.addAction("$packageName.ir") // InAppMessage received + i.addAction("$packageName.ic") // InAppMessage clicked + i.addAction("$packageName.it") // InAppMessage triggered + i.addAction("$packageName.id") // InAppMessage dismissed + i.addAction("$packageName.ibc") // InAppMessage button clicked + context.registerReceiver(this, i) + } + + private val notificationTypes = listOf("IdType.DeviceId", "IdType.GoogleAdvertisingId", "IdType.CustomId") + private val eventTypes = listOf("EventAction.custom", "EventAction.sign_up", "EventAction.login", "EventAction.purchase", "EventAction.achievement", "EventAction.level") + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val methodName = call.method + val notificationModule = Pushe.getPusheService(PusheNotification::class.java) + val analyticsModule = Pushe.getPusheService(PusheAnalytics::class.java) + val piamModule = Pushe.getPusheService(PusheInAppMessaging::class.java) + + @Suppress("DEPRECATION") // Will notify developer in dart code + when (methodName) { + "Pushe.initialize" -> Pushe.initialize() + "Pushe.setUserConsentGiven" -> setUserConsentGiven(call, result) + "Pushe.getUserConsentStatus" -> result.success(Pushe.getUserConsentStatus()) + "Pushe.getDeviceId" -> result.success(Pushe.getDeviceId()) + "Pushe.getAndroidId" -> result.success(Pushe.getAndroidId()) + "Pushe.getGoogleAdvertisingId" -> result.success(Pushe.getGoogleAdvertisingId()) + "Pushe.getCustomId" -> result.success(Pushe.getCustomId()) + "Pushe.setCustomId" -> setCustomId(call, result) + "Pushe.getUserEmail" -> result.success(Pushe.getUserEmail()) + "Pushe.setUserEmail" -> setUserEmail(call, result) + "Pushe.getUserPhoneNumber" -> result.success(Pushe.getUserPhoneNumber()) + "Pushe.setUserPhoneNumber" -> setUserPhoneNumber(call, result) + "Pushe.subscribe" -> subscribeToTopic(call, result) + "Pushe.unsubscribe" -> unsubscribeFromTopic(call, result) + "Pushe.enableNotifications" -> setNotificationEnabled(true, result, notificationModule) + "Pushe.disableNotifications" -> setNotificationEnabled(false, result, notificationModule) + "Pushe.enableCustomSound" -> setCustomSoundEnabled(true, result, notificationModule) + "Pushe.disableCustomSound" -> setCustomSoundEnabled(false, result, notificationModule) + "Pushe.isNotificationEnable" -> isNotificationEnabled(result, notificationModule) + "Pushe.isCustomSoundEnabled" -> isCustomSoundEnabled(result, notificationModule) + "Pushe.createNotificationChannel" -> createNotificationChannel(call, result, notificationModule) + "Pushe.removeNotificationChannel" -> removeNotificationChannel(call, result, notificationModule) + "Pushe.isInitialized" -> result.success(Pushe.isInitialized()) + "Pushe.isRegistered" -> result.success(Pushe.isRegistered()) + "Pushe.sendUserNotification" -> sendUserNotification(call, result, notificationModule) + "Pushe.sendAdvancedUserNotification" -> sendAdvancedNotification(call, result, notificationModule) + "Pushe.sendEvent" -> sendEvent(call, result, analyticsModule) + "Pushe.sendEcommerceData" -> sendEcommerceData(call, result, analyticsModule) + "Pushe.initNotificationListenerManually" -> initializeForegroundNotifications() + "Pushe.setRegistrationCompleteListener" -> Pushe.setRegistrationCompleteListener { + result.success(true) + } + "Pushe.setInitializationCompleteListener" -> Pushe.setInitializationCompleteListener { + result.success(true) + } + "Pushe.addTags" -> addTag(call, result) + "Pushe.removeTags" -> removeTags(call, result) + "Pushe.getSubscribedTags" -> result.success(Pushe.getSubscribedTags()) + "Pushe.getSubscribedTopics" -> result.success(Pushe.getSubscribedTopics()) + "Pushe.notificationListener" -> setNotificationListeners(call, result) + // Background init + "Pushe.platformInitialized" -> initializeListenerPlatform(result) + // InAppMessaging + "Pushe.triggerEvent" -> triggerEvent(call, result, piamModule) + "Pushe.disableInAppMessaging" -> disableInAppMessaging(result, piamModule) + "Pushe.enableInAppMessaging" -> enableInAppMessaging(result, piamModule) + "Pushe.isInAppMessagingEnabled" -> isInAppMessagingEnabled(result, piamModule) + "Pushe.initializeInAppListeners" -> initializeInAppMessagingListeners() + "Pushe.dismissShownInApp" -> dismissShownInApp(result, piamModule) + "Pushe.testInAppMessage" -> testInAppMessage(call, result, piamModule) + else -> result.notImplemented() + } + } + + ///// Do stuff when a method was called + + private fun setUserConsentGiven(call: MethodCall, result: MethodChannel.Result) { + try { + Pushe.setUserConsentGiven(call.argument("enabled") ?: true) + result.success(true) + } catch (e: java.lang.Exception) { + result.error("022", "Error occorred when parsing `enabled` argument. Must be of type bool", e.message) + } + } + + private fun setCustomId(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("id")) { + Pushe.setCustomId(null) + result.success(true) + return + } + Pushe.setCustomId(call.argument("id")) + result.success(true) + } + + private fun setUserEmail(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("email")) { + result.success(Pushe.setUserEmail(null)) + return + } + result.success(Pushe.setUserEmail(call.argument("email"))) + } + + private fun setUserPhoneNumber(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("phone")) { + result.success(Pushe.setUserPhoneNumber(null)) + return + } + result.success(Pushe.setUserPhoneNumber(call.argument("phone"))) + } + + private fun subscribeToTopic(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("topic")) { + result.error("004", "Call must contain 'topic'", null) + return + } + try { + Pushe.subscribeToTopic(call.argument("topic")) { + result.success(true) + } + } catch (e: Exception) { + result.error("004", "Could not subscribe to topic ${e.message}", null) + } + } + + private fun unsubscribeFromTopic(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("topic")) { + result.error("005", "Call must contain 'topic'", null) + return + } + try { + Pushe.unsubscribeFromTopic(call.argument("topic")) { + result.success(true) + } + } catch (e: Exception) { + result.error("005", "Could not unsubscribe from topic ${e.message}", null) + } + } + + private fun setNotificationEnabled(enabled: Boolean, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("013", "Notification module is not ready. Notifications will not ba handled.", null) + return + } + if (enabled) { + notificationModule.enableNotifications() + } else { + notificationModule.disableNotifications() + } + } + + private fun isNotificationEnabled(result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("013", "Notification module is not ready. Notifications will not ba handled.", null) + return + } + result.success(notificationModule.isNotificationEnable()) + } + + private fun sendUserNotification(call: MethodCall, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("006", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + + if (!call.hasAll("type", "id", "title", "content")) { + result.error("006", "Call must contain 'type', 'id', 'title' and 'content'", null) + return + } + + val type = call.argument("type") + lg("Type: $type") + if (!notificationTypes.contains(type)) { + result.error("006", "Type must be either 'DeviceId', 'GoogleAdvertisingId' or 'CustomId'", null) + return + } + + val id = call.argument("id") + if (id == null || id.isEmpty()) { + result.error("006", "Id must not be null or empty", null) + return + } + + val notification = when (type) { + "IdType.DeviceId" -> UserNotification.withAndroidId(id) + "IdType.GoogleAdvertisingId" -> UserNotification.withAdvertisementId(id) + "IdType.CustomId" -> UserNotification.withCustomId(id) + else -> { + result.error("006", "Type must be either 'DeviceId', 'GoogleAdvertisingId' or 'CustomId'", null) + return + } + } + + val title = call.argument("title") + val content = call.argument("content") + val bigTitle = call.argument("bigTitle") + val bigContent = call.argument("bigContent") + val imageUrl = call.argument("imageUrl") + val iconUrl = call.argument("iconUrl") + val notifIcon = call.argument("notifIcon") + val customContent = call.argument("customContent") + notification?.setTitle(title) + ?.setContent(content) + ?.setBigTitle(bigTitle) + ?.setBigContent(bigContent) + ?.setImageUrl(imageUrl) + ?.setIconUrl(iconUrl) + ?.setNotifIcon(notifIcon) + ?.setCustomContent(customContent) + if (notification != null) { + notificationModule.sendNotificationToUser(notification) + result.success(true) + } else { + result.error("006", "There was a problem building notification for sending. Make sure data is provided correctly", null) + } + } + + private fun sendAdvancedNotification(call: MethodCall, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("020", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + if (!call.hasAll("type", "id", "advancedJson")) { + result.error("020", "Call must contain 'type', 'id' and 'advancedJson' (Use jsonEncode for advancedJson)", null) + return + } + + val type = call.argument("type") + if (!notificationTypes.contains(type)) { + result.error("020", "Type must be either 'DeviceId', 'GoogleAdvertisingId' or 'CustomId'", null) + return + } + + val id = call.argument("id") + if (id == null || id.isEmpty()) { + result.error("020", "Id must not be null or empty", null) + return + } + val notification = when (type) { + "IdType.DeviceId" -> UserNotification.withAndroidId(id) + "IdType.GoogleAdvertisingId" -> UserNotification.withAdvertisementId(id) + "IdType.CustomId" -> UserNotification.withCustomId(id) + else -> { + result.error("006", "Type must be either 'DeviceId', 'GoogleAdvertisingId' or 'CustomId'", null) + return + } + } + val advancedJson = call.argument("advancedJson") + notification.setAdvancedNotification(advancedJson) + notificationModule.sendNotificationToUser(notification) + + } + + private fun sendEvent(call: MethodCall, result: MethodChannel.Result, analyticsModule: PusheAnalytics?) { + if (analyticsModule == null) { + result.error("016", "Analytics module is not ready. Analytics APIs will not ba handled.", null) + return + } + if (!call.hasArgument("name")) { + result.error("016", "Call must contain 'name', only data and action are optional", null) + return + } + val name = call.argument("name") + if (name == null || name.isEmpty()) { + result.error("016", "Call must contain 'name', only data and action are optional", null) + return + } + val action = call.argument("action") + + val actualAction = when (action?.removePrefix("EventAction.")) { + "custom" -> EventAction.CUSTOM + "sign_up" -> EventAction.SIGN_UP + "login" -> EventAction.LOGIN + "purchase" -> EventAction.PURCHASE + "achievement" -> EventAction.ACHIEVEMENT + "level" -> EventAction.LEVEL + else -> EventAction.CUSTOM + } + + val eventBuilder = Event.Builder(name) + eventBuilder.setAction(actualAction) + call.argument>("data")?.let { + eventBuilder.setData(it) + } + analyticsModule.sendEvent(eventBuilder.build()) + result.success(true) + } + + private fun sendEcommerceData(call: MethodCall, result: MethodChannel.Result, analyticsModule: PusheAnalytics?) { + if (analyticsModule == null) { + result.error("018", "Analytics module is not ready. Analytics APIs will not ba handled.", null) + return + } + + if (!call.hasAll("name", "price")) { + result.error("018", "Call must contain 'name' and 'price'", null) + return + } + val name = call.argument("name") + val price = call.argument("price") + + if (name == null || price == null) { + result.error("018", "'name' and 'price' can not be null", null) + return + } + + val ecommerceBuilder = Ecommerce.Builder(name, price) + if (call.hasArgument("category") && call.argument("category")?.isNotBlank() == true) { + ecommerceBuilder.setCategory(call.argument("category")) + } + if (call.hasArgument("quantity") && call.argument("quantity") != null) { + ecommerceBuilder.setQuantity(call.argument("category")) + } + + analyticsModule.sendEcommerceData(ecommerceBuilder.build()) + result.success(true) + } + + private fun initializeListenerPlatform(result: MethodChannel.Result) { + PusheNotificationListener.onInitialized(context) + result.success(true) + } + + private fun addTag(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("tags") || call.argument("tags") == null || call.argument("tags") !is Map<*, *>) { + result.error("012", "Failed to add tags. No valid tags provided.", null) + return + } + val tags: Map? = call.argument>("tags") + Pushe.addTags(tags) { + result.success(true) + return@addTags + } + } + + private fun removeTags(call: MethodCall, result: MethodChannel.Result) { + if (!call.hasArgument("tags") && call.argument("tags") !is List<*>) { + result.error("012", "Failed to remove tags. No tags provided.", null) + return + } + val tags = call.argument>("tags")!! + Pushe.removeTags(tags) { + result.success(true) + } + } + + private fun setCustomSoundEnabled(enabled: Boolean, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("017", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + if (enabled) { + notificationModule.enableCustomSound() + } else { + notificationModule.disableCustomSound() + } + result.success(true) + } + + private fun isCustomSoundEnabled(result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("017", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + result.success(notificationModule.isCustomSoundEnable()) + } + + private fun createNotificationChannel(call: MethodCall, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("019", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + + if (!call.hasAll("channelId", "channelName")) { + result.error("019", "Call must contain 'channelId' and 'channelName'", null) + return + } + + try { + val channelId: String = call.argument("channelId") ?: "" + val channelName: String = call.argument("channelName") ?: "" + val description: String? = call.argument("description") + val importance: Int = call.argument("importance") ?: -1 + val enableLight: Boolean = call.argument("enableLight") ?: false + val enableVibration: Boolean = call.argument("enableVibration") ?: false + val showBadge: Boolean = call.argument("showBadge") ?: false + val ledColor: Int = call.argument("ledColor") ?: 0 + val vibrationPattern: LongArray? = call.argument("vibrationPattern") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationModule.createNotificationChannel(channelId, channelName, description, importance, enableLight, enableVibration, showBadge, ledColor, vibrationPattern) + } + result.success(true) + } catch (e: Exception) { + result.error("019", "Could not create notification channel.\n ${e.message}", null) + } + } + + private fun removeNotificationChannel(call: MethodCall, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("021", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + if (!call.hasArgument("channelId") || call.argument("channelId") == null) { + result.error("021", "Call must contain 'channelId' which is not null.", null) + return + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationModule.removeNotificationChannel(call.argument("channelId") as String) + } + result.success(true) + } + + private fun setNotificationListeners(call: MethodCall, result: MethodChannel.Result) { + var setupCallbackHandle: Long = 0 + var backgroundMessageHandle: Long = 0 + try { + val setup = call.argument("setupHandle") + val background = call.argument("backgroundHandle") + setupCallbackHandle = if (setup is Int) { + setup.toLong() + } else { + setup as Long + } + backgroundMessageHandle = if (background is Int) { + background.toLong() + } else { + background as Long + } + } catch (e: Exception) { + lg("There was an exception when getting callback handle from Dart side") + e.printStackTrace() + } + saveSetupHandle(context, setupCallbackHandle) + PusheNotificationListener.startBackgroundIsolate(context, setupCallbackHandle) + saveMessageHandle(context, backgroundMessageHandle) + result.success(true) + } + + // region InAppMessaging + private fun triggerEvent(call: MethodCall, result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("023", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } else if(!call.hasArgument("event") || call.argument("event") == null) { + result.error("023", "Triggering event requires Non-null 'event' argument. Call does not have any.", null) + return + } + piamModule.triggerEvent(call.argument("event") ?: "") + result.success(true) + } + + private fun disableInAppMessaging(result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("024", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } + piamModule.disableInAppMessaging() + result.success(true) + } + + private fun enableInAppMessaging(result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("024", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } + piamModule.enableInAppMessaging() + result.success(true) + } + + private fun isInAppMessagingEnabled(result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("025", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } + result.success(piamModule.isInAppMessagingEnabled()) + } + + fun dismissShownInApp(result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("026", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } + piamModule.dismissShownInApp() + result.success(true) + } + + @VisibleForTesting + fun testInAppMessage(call: MethodCall, result: MethodChannel.Result, piamModule: PusheInAppMessaging?) { + if(piamModule == null) { + result.error("027", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } else if(!call.hasArgument("message") || call.argument("message") == null) { + result.error("023", "Testing in app message requires Non-null 'message' argument. Call does not have any.", null) + return + } + piamModule.testInAppMessage(call.argument("message") ?: "", call.argument("instant") ?: false) + } + + fun initializeInAppMessagingListeners() { + PusheInAppMessagingListener.setListeners(context) + } + + // endregion + + private fun initializeForegroundNotifications() { + PusheNotificationListener.setNotificationCallbacks(context.applicationContext) + } + + ///// Do stuff when a callback was received + + /** + * Handle foreground notification callbacks + * Callback are broadcasted, so this will receive the broadcast and send the parsed intent data as json to the dart side. + */ + override fun onReceive(context: Context, intent: Intent) { + val channel = MethodChannel(messenger, "plus.pushe.co/pushe_flutter") + val packageName = context.packageName + FlutterMain.ensureInitializationComplete(context, null) + val action = if (intent.action == null) "" else intent.action + if (action.isEmpty()) { + return + } + + when (action) { + "$packageName.nr" -> { + channel.invokeMethod("Pushe.onNotificationReceived", getNotificationJsonFromIntent(intent).toString()) + } + "$packageName.nc" -> { + channel.invokeMethod("Pushe.onNotificationClicked", getNotificationJsonFromIntent(intent).toString()) + } + "$packageName.nbc" -> { + channel.invokeMethod("Pushe.onNotificationButtonClicked", getNotificationJsonFromIntent(intent).toString()) + } + "$packageName.nccr" -> { + channel.invokeMethod("Pushe.onCustomContentReceived", getCustomContentFromIntent(intent).toString()) + } + "$packageName.nd" -> { + channel.invokeMethod("Pushe.onNotificationDismissed", getNotificationJsonFromIntent(intent).toString()) + } + + "$packageName.ir" -> { + channel.invokeMethod("Pushe.inAppMessageReceived", getInAppMessageFromIntent(intent).toString()) + } + "$packageName.ic" -> { + channel.invokeMethod("Pushe.inAppMessageClicked", getInAppMessageFromIntent(intent).toString()) + } + "$packageName.it" -> { + channel.invokeMethod("Pushe.inAppMessageTriggered", getInAppMessageFromIntent(intent).toString()) + } + "$packageName.id" -> { + channel.invokeMethod("Pushe.inAppMessageDismissed", getInAppMessageFromIntent(intent).toString()) + } + "$packageName.ibc" -> { + channel.invokeMethod("Pushe.inAppMessageButtonClicked", getInAppMessageAndButtonFromIntent(intent).toString()) + } + } + } + + ///// Utils + + /** + * Check that if the the MethodCall which was received from dart side, contains all the keys in the arguement. + */ + private fun MethodCall.hasAll(vararg keys: String): Boolean { + var hasAll = true + keys.forEach { + if (!hasArgument(it)) { + hasAll = false + return@forEach + } + } + return hasAll + } + + +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterApplication.java b/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterApplication.java new file mode 100644 index 0000000..0d13b43 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterApplication.java @@ -0,0 +1,31 @@ +package co.pushe.plus.flutter; + +import android.content.Context; + +import androidx.multidex.MultiDex; + +import io.flutter.app.FlutterApplication; +import io.flutter.plugin.common.PluginRegistry; + +import static co.pushe.plus.flutter.PusheFlutterPlugin.initialize; + +public class PusheFlutterApplication extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(this); + } + + @Override + public void onCreate() { + super.onCreate(); + PusheFlutterPlugin.setDebugMode(false); + initialize(this); + } + + @Override + public void registerWith(PluginRegistry registry) { + PusheFlutterPlugin.registerWith(registry); + } +} diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterPlugin.kt b/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterPlugin.kt new file mode 100644 index 0000000..4607277 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheFlutterPlugin.kt @@ -0,0 +1,123 @@ +package co.pushe.plus.flutter + +import android.content.Context +import android.util.Log +import co.pushe.plus.flutter.Utils.lg +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback +import io.flutter.plugin.common.PluginRegistry.Registrar + +/** + * PusheFlutterPlugin + * Main class that the developer who uses this plugin interacts with. + * Class handles all configuration stuff which developer will do. + * Mainly used functions are: + * [PusheFlutterPlugin.initialize] + * [PusheFlutterPlugin.debugMode] + * [PusheFlutterPlugin.registerWith] // both types + * @author Mahdi Malvandi + */ +@Suppress("SpellCheckingInspection") +class PusheFlutterPlugin : FlutterPlugin, ActivityAware { + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + setUpChannel(binding.applicationContext, binding.binaryMessenger) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + PusheLifeCycle.isForeground = false + } + + ///// Activity aware functions + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + PusheLifeCycle.isForeground = true + } + + override fun onDetachedFromActivityForConfigChanges() { + PusheLifeCycle.isForeground = false + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + PusheLifeCycle.isForeground = true + } + + override fun onDetachedFromActivity() { + PusheLifeCycle.isForeground = false + } + + companion object { + + /** + * In order to be able to use the library callbacks, you need to override the application class and make a custom one. + * 1. Create a class called `App` in the root of `android/app/packageName`, + * extend [io.flutter.app.FlutterApplication] and implement [io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback]. + * 2. Introduce it to the `AndroidManifest.xml` located at, `android/app/src/main/` as `android:name=".App" (or the full packageName of the class) + * 3. Add [PusheFlutterPlugin.initialize] in `onCreate` method of the app class (override it if it does not exist). + * 4. Add [PusheFlutterPlugin.registerWith] and pass the registry in the `registerWith` method (override it if it does not exist). + * @param initializer is the instance of your app which follows the extending and implementing rule above. + * @param forces the instance to firstly, extend the FlutterApplication (Context) and secondly, implement PluginRegistrantCallback + * 5. You're good at native side, you may follow the rest according to https://docs.pushe.co + * -- Why is it here? -- To make usage more simple, this class will handle all the stuff and user does not need to know about any other classes. + */ + @JvmStatic + fun initialize(initializer: T) where T : Context, T : PluginRegistrantCallback { + PusheNotificationListener.initialize(initializer) + } + + /** + * Registration for v1 embedded flutter projects + * Might cause issue with proguard, so you better exclude it in the rules by adding + * ``` + * -keep co.pushe.plus.flutter.** { *; } + * ``` + */ + @JvmStatic + fun registerWith(registry: PluginRegistry) { + registerWith(registry.registrarFor("co.pushe.plus.flutter.PusheFlutterPlugin")) + } + + @JvmStatic + fun registerWith(registrar: Registrar) { + lg("Plugin registered using 'registerWith' static method") + setUpChannel(registrar.context(), registrar.messenger()) + } + + @JvmStatic + internal fun setUpChannel(context: Context?, messenger: BinaryMessenger?) { + if (context == null || messenger == null) { + Log.e("Pushe", "Unhandled exception occurred.\n" + + "Either BinaryMessenger or Android Context is null. So plugin can not set MessageHandlers") + return + } + // Main Method handler + val channel = MethodChannel(messenger, "plus.pushe.co/pushe_flutter") + val callHandler = PusheChandler(context, messenger) + channel.setMethodCallHandler(callHandler) + + // When background was applied + val backgroundChannel = MethodChannel(messenger, "plus.pushe.co/pushe_flutter_background") + backgroundChannel.setMethodCallHandler(callHandler) + PusheNotificationListener.setBackgroundChannel(backgroundChannel) + } + + /** + * If set to true, + * verbose logs will be printed to logcat, + * so each step can be tracked and observed. + */ + @JvmStatic + var debugMode = false + + @JvmStatic + fun appOnForeground(foreground: Boolean) { + PusheLifeCycle.isForeground = foreground + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/PusheInAppMessagingListener.kt b/android/src/main/kotlin/co/pushe/plus/flutter/PusheInAppMessagingListener.kt new file mode 100644 index 0000000..2ac489b --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheInAppMessagingListener.kt @@ -0,0 +1,68 @@ +package co.pushe.plus.flutter + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import co.pushe.plus.Pushe +import co.pushe.plus.flutter.InAppUtils.getInAppMessage +import co.pushe.plus.inappmessaging.InAppMessage +import co.pushe.plus.inappmessaging.PusheInAppMessaging +import co.pushe.plus.inappmessaging.PusheInAppMessagingListener +import io.flutter.view.FlutterMain + +object PusheInAppMessagingListener { + + fun setListeners(context: Context) { + val packageName = context.packageName + val inAppMessaging = Pushe.getPusheService(PusheInAppMessaging::class.java) + inAppMessaging?.setInAppMessagingListener(object : PusheInAppMessagingListener { + override fun onInAppMessageReceived(inAppMessage: InAppMessage) { + val message = getInAppMessage(inAppMessage) + handleForegroundMessage(context, "$packageName.ir", "piam" to message.toString()) + } + + override fun onInAppMessageTriggered(inAppMessage: InAppMessage) { + val message = getInAppMessage(inAppMessage) + handleForegroundMessage(context, "$packageName.it", "piam" to message.toString()) + } + + override fun onInAppMessageDismissed(inAppMessage: InAppMessage) { + val message = getInAppMessage(inAppMessage) + handleForegroundMessage(context, "$packageName.id", "piam" to message.toString()) + } + + override fun onInAppMessageButtonClicked(inAppMessage: InAppMessage, index: Int) { + val message = getInAppMessage(inAppMessage) + handleForegroundMessage(context, "$packageName.ibc", + "piam" to message.toString(), + "index" to index.toString() + ) + } + + override fun onInAppMessageClicked(inAppMessage: InAppMessage) { + val message = getInAppMessage(inAppMessage) + handleForegroundMessage(context, "$packageName.ic", "piam" to message.toString()) + } + + }) + } + + /** + * If native callbacks were called when app is on the foreground, this function will handle the sending. + * The method will broadcase an intent to throughout the app and the receiver (PusheFlutterPlugin itself) + * will receive it and send it to the dart side for execution. + */ + private fun handleForegroundMessage(context: Context, action: String, vararg data: Pair) { + val main = Handler(Looper.getMainLooper()) + main.post { + FlutterMain.ensureInitializationComplete(context, null) + val i = Intent(action) + i.setPackage(context.packageName) + for (datum in data) { + i.putExtra(datum.first, datum.second) + } + context.sendBroadcast(i) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/PusheNotificationListener.kt b/android/src/main/kotlin/co/pushe/plus/flutter/PusheNotificationListener.kt new file mode 100644 index 0000000..75ab42b --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheNotificationListener.kt @@ -0,0 +1,319 @@ +package co.pushe.plus.flutter + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.util.Log +import co.pushe.plus.Pushe +import co.pushe.plus.flutter.HandleStorage.getMessageHandle +import co.pushe.plus.flutter.HandleStorage.getSetupHandle +import co.pushe.plus.flutter.HandleStorage.hasSetupHandle +import co.pushe.plus.flutter.Pack.getBackgroundNotificationObject +import co.pushe.plus.flutter.Pack.getButtonJsonObject +import co.pushe.plus.flutter.Pack.getCustomContent +import co.pushe.plus.flutter.Pack.getNotificationJsonObject +import co.pushe.plus.flutter.Utils.isAppOnForeground +import co.pushe.plus.flutter.Utils.lg +import co.pushe.plus.notification.NotificationButtonData +import co.pushe.plus.notification.NotificationData +import co.pushe.plus.notification.PusheNotification +import co.pushe.plus.notification.PusheNotificationListener +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback +import io.flutter.view.FlutterCallbackInformation +import io.flutter.view.FlutterMain +import io.flutter.view.FlutterNativeView +import io.flutter.view.FlutterRunArguments +import org.json.JSONObject +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicBoolean + +@Suppress("SpellCheckingInspection") +internal object PusheNotificationListener { + + private const val TAG = "Pushe" + private var backgroundFlutterView: FlutterNativeView? = null + private var pluginRegistrantCallback: PluginRegistrantCallback? = null + private val isIsolateRunning = AtomicBoolean(false) + private var backgroundMessageHandle: Long? = null + private val backgroundMessageQueue = Collections.synchronizedList(LinkedList()) + private var backgroundChannel: MethodChannel? = null + private var result: MethodChannel.Result? = null +// private var engine: FlutterEngine? = null + + /** + * In order to be able to use the library callbacks, you need to override the application class and make a custom one. + * 1. Create a class called `App` in the root of `android/app/packageName`, + * extend `io.flutter.app.FlutterApplication` and implement `io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback`. + * 2. Introduce it to the `AndroidManifest.xml` located at, `android/app/src/main/` as `android:name=".App" (or the full packageName of the class) + * 3. Add `PusheFlutterPlugin.initialize(this)` in `onCreate` method of the app class (override it if it does not exist). + * 4. Add `PusheFlutterPlugin.registerWith(registry);` in the `registerWith` method (override it if it does not exist). + * @param initializer is the instance of your app which follows the extending and implementing rule above. + * @param forces the instance to firstly, extend the FlutterApplication (Context) and secondly, implement PluginRegistrantCallback + * 5. You're good at native side, you may follow the rest due to [ https://docs.pushe.co ] + */ + fun initialize(initializer: T) where T : Context, T : PluginRegistrantCallback { + setPluginRegistrant(initializer) + setNotificationCallbacks(initializer) + } + + /** + * Initializes native sdk listeners. + * SDK will call these callbacks and plugin will handle the sending stuff. + */ + fun setNotificationCallbacks(context: Context) { + val c = context.applicationContext + FlutterMain.ensureInitializationComplete(c, null) +// engine = FlutterEngine(c) + + // TODO(MahdiMalv): Do we need to register all plugins in the background? +// if (pluginRegistrantCallback != null) { // Register all plugins +// pluginRegistrantCallback!!.registerWith(ShimPluginRegistry(engine!!)) +// } + if (hasSetupHandle(context)) { + startBackgroundIsolate(c, getSetupHandle(c)) + } + val pusheNotification = Pushe.getPusheService(PusheNotification::class.java) + if (pusheNotification == null) { + Log.e("Pushe", "Pushe notification module is not found") + return + } + pusheNotification.setNotificationListener(object : PusheNotificationListener { + override fun onNotification(notificationData: NotificationData) { + val type = Constants.RECEIVE + if (isAppOnForeground()) { + lg("Notification is received in the foreground") + val message = getNotificationJsonObject(notificationData) + if (message == null) { + Log.e(TAG, "onNotification: Failed to get message of callback") + return + } + handleForegroundMessage(c, c.packageName + ".nr", "data" to message.toString()) + } else { + lg("Notification is received in the background") + val backgroundMessage = getBackgroundNotificationObject(notificationData, type, null) + if (backgroundMessage == null) { + Log.e(TAG, "onNotification: Failed to get message of callback") + return + } + handleBackgroundMessage(c, backgroundMessage.toString()) + } + } + + override fun onCustomContentNotification(customContent: Map) { + if (isAppOnForeground()) { + handleForegroundMessage(c, c.packageName + ".nccr", "json" to JSONObject(customContent).toString()) + } else { + lg("Custom content received in the background") + val customContentObject = getCustomContent(customContent) + // Type is specified already + handleBackgroundMessage(c, customContentObject.toString()) + } + } + + override fun onNotificationClick(notificationData: NotificationData) { + val type = Constants.CLICK + if (isAppOnForeground()) { + lg("Notification is clicked in the foreground") + val message = getNotificationJsonObject(notificationData) + if (message == null) { + Log.e(TAG, "onNotificationClick: Failed to get message of callback") + return + } + handleForegroundMessage(c, c.packageName + ".nc", "data" to message.toString()) + } else { + lg("Notification is clicked in the background") + val backgroundMessage = getBackgroundNotificationObject(notificationData, type, null) + if (backgroundMessage == null) { + Log.e(TAG, "onNotificationClick: Failed to get message of callback") + return + } + handleBackgroundMessage(c, backgroundMessage.toString()) + } + } + + override fun onNotificationDismiss(notificationData: NotificationData) { + val type = Constants.DISMISS + if (isAppOnForeground()) { + lg("Notification is dismissed in the foreground") + val message = getNotificationJsonObject(notificationData) + if (message == null) { + Log.e(TAG, "onNotificationClick: Failed to get message of callback") + return + } + handleForegroundMessage(c, c.packageName + ".nd", "data" to message.toString()) + } else { + lg("Notification is dismissed in the background") + val backgroundMessage = getBackgroundNotificationObject(notificationData, type, null) + if (backgroundMessage == null) { + Log.e(TAG, "onNotificationDismiss: Failed to get message of callback") + return + } + handleBackgroundMessage(c, backgroundMessage.toString()) + } + } + + override fun onNotificationButtonClick(notificationButtonData: NotificationButtonData, notificationData: NotificationData) { + val type = Constants.BUTTON_CLICK + if (isAppOnForeground()) { + lg("Notification button is clicked in the foreground") + val message = getNotificationJsonObject(notificationData, notificationButtonData) + if (message == null) { + Log.e(TAG, "onNotificationButtonClick: Failed to get message or clicked button of callback") + return + } + handleForegroundMessage(c, c.packageName + ".nbc", "data" to message.toString()) + } else { + lg("Notification button is clicked in the background") + val backgroundMessage = getBackgroundNotificationObject(notificationData, type, notificationButtonData) + if (backgroundMessage == null) { + Log.e(TAG, "onNotificationButtonClick: Failed to get message of callback") + return + } + handleBackgroundMessage(c, backgroundMessage.toString()) + } + } + }) + } + + /** + * Called when platform is ready and there is an isolate to run dart code, so any waiting message will run instantly. + * It will iterate over the wating messages and execute them one by one. + */ + fun onInitialized(context: Context) { + lg("Plugin initialized. Platform isolate is running") + isIsolateRunning.set(true) + synchronized(backgroundMessageQueue) { + if (backgroundMessageQueue.isEmpty()) { + return@synchronized + } + lg("Iterating over pending messages to execute") + for (s in backgroundMessageQueue) { + sendBackgroundMessageToExecute(context, s, null) + } + backgroundMessageQueue.clear() + } + } + + /** + * If native callbacks were called when app is on the foreground, this function will handle the sending. + * The method will broadcase an intent to throughout the app and the receiver (PusheFlutterPlugin itself) + * will receive it and send it to the dart side for execution. + */ + private fun handleForegroundMessage(context: Context, action: String, vararg data: Pair) { + val main = Handler(Looper.getMainLooper()) + main.post { + FlutterMain.ensureInitializationComplete(context, null) + val i = Intent(action) + i.setPackage(context.packageName) + for (datum in data) { + i.putExtra(datum.first, datum.second) + } + context.sendBroadcast(i) + } + } + + /** + * If native callbacks were called when app is in the background, this function will handle the sending. + * The method will wake up flutter engine and tries to initialize the dart side by calling the setup method + * using its saved handle key. The setup will then call the dart side method which is passed in by the developer. + */ + private fun handleBackgroundMessage(c: Context, backgroundMessage: String) { + if (!isIsolateRunning.get()) { + lg("Isolate is not running, adding message to queue") + backgroundMessageQueue.add(backgroundMessage) + } else { + lg("Isolate is running, executing message") + val latch = CountDownLatch(1) + sendBackgroundMessageToExecute(c, backgroundMessage, latch) + try { + latch.await() + } catch (ex: InterruptedException) { + Log.i(TAG, "Exception waiting to execute Dart callback", ex) + } + } + } + + /** + * Runs the setup callback method in background which will start the Pushe's top level function + * The top level function will call `onInitialized` of this class and by calling it, isolate will be ready for execution and wating messages will be sent. + */ + @Suppress("SENSELESS_COMPARISON") + fun startBackgroundIsolate(context: Context, callbackHandle: Long) { + FlutterMain.ensureInitializationComplete(context, null) + val appBundlePath = FlutterMain.findAppBundlePath() + val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) + if (flutterCallback == null) { + Log.e(TAG, "Fatal: failed to find callback") + return + } + + backgroundFlutterView = FlutterNativeView(context, true) + if (appBundlePath != null && !isIsolateRunning.get()) { + if (pluginRegistrantCallback == null) { + lg("Fatal: pluginRegistrantCallback is not set. Background callback will not be initialized.\n" + + "You must override your application class and call `initialize`." + + " Checkout https://docs.pushe.co for more info.") + return + } + val args = FlutterRunArguments() + args.bundlePath = appBundlePath + args.entrypoint = flutterCallback.callbackName + args.libraryPath = flutterCallback.callbackLibraryPath + + if (backgroundFlutterView == null) { + lg("Failed to configure native view") + return + } + + backgroundFlutterView?.runFromBundle(args) + pluginRegistrantCallback?.registerWith(backgroundFlutterView?.pluginRegistry) + } + } + + + /** + * This function uses a background channel to send native messages to dart side. The dart side setup function will handle the rest. + * Will be available as soon as the `onInitialized` gets called. + */ + private fun sendBackgroundMessageToExecute(context: Context, message: String, latch: CountDownLatch?) { + lg("sendBackgroundMessageToExecute: Sending background message to Dart side") + if (backgroundChannel == null) { + Log.e(TAG, "Fatal: background channel is not set. Flutter callback will not be executed.\n" + + " This means the PusheFlutterPlugin is not registered when app is started." + + " You must override you application class and call `initialize` in the onCreate." + + " Checkout https://docs.pushe.co for more info.") + return + } + // If another thread is waiting, then wake that thread when the callback returns a result. + if (latch != null) { + result = LatchResult(latch).result + } + val args: MutableMap = HashMap() + if (backgroundMessageHandle == null) { + backgroundMessageHandle = getMessageHandle(context) + } + args["handle"] = backgroundMessageHandle + args["message"] = message + backgroundChannel?.invokeMethod("handleBackgroundMessage", args, null) + latch?.countDown() + } + + /** + * Initializes background channle (for sending messages to dart side) + */ + fun setBackgroundChannel(channel: MethodChannel) { + backgroundChannel = channel + } + + /** + * PluginRegistrantCallback is a necessary component for background execution. + * It will register the background native view so it will be able to call setup method in the dart side. + */ + fun setPluginRegistrant(callback: PluginRegistrantCallback) { + lg("setPluginRegistrant: pluginRegistrantCallback initialized") + pluginRegistrantCallback = callback + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/co/pushe/plus/flutter/Utils.kt b/android/src/main/kotlin/co/pushe/plus/flutter/Utils.kt new file mode 100644 index 0000000..1efa153 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/Utils.kt @@ -0,0 +1,232 @@ +package co.pushe.plus.flutter + +import android.content.Intent +import android.util.Log +import co.pushe.plus.flutter.Utils.lg +import co.pushe.plus.inappmessaging.InAppMessage +import co.pushe.plus.notification.NotificationButtonData +import co.pushe.plus.notification.NotificationData +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** + * This static class will do all tasks related to parsing and packing. + */ +internal object Pack { + + @JvmStatic + fun getNotificationJsonObject(data: NotificationData?, clickedButtonData: NotificationButtonData? = null): JSONObject? { + if (data == null) { + return null + } + val notificationObject = JSONObject() + return try { + notificationObject.apply { + put("title", data.title) + put("content", data.content) + put("bigTitle", data.bigTitle) + put("bigContent", data.bigContent) + put("imageUrl", data.imageUrl) + put("summary", data.summary) + put("iconUrl", data.iconUrl) + + if (clickedButtonData != null) { + try { + put("clickedButton", getButtonJsonObject(clickedButtonData)) + } catch (e: Exception) { + lg("[Parsing notification] Failed to add button to the notification data") + } + } + try { + put("json", JSONObject(data.customContent)) + } catch (e: Exception) { + lg("[Parsing notification] No json to put as customContent field") + } + try { + put("buttons", getButtonsJsonArray(data.buttons)) + } catch (e: Exception) { + lg("[Parsing notification] No buttons to put as customContent field") + } + } + notificationObject + } catch (e: JSONException) { + Log.w("Pushe", "Failed to parse notification and convert it to Json.") + null + } + } + + @JvmStatic + fun getButtonJsonObject(buttonData: NotificationButtonData?): JSONObject? { + if (buttonData == null) { + return null + } + val o = JSONObject() + return try { + o.run { + put("title", buttonData.text) + put("icon", buttonData.icon) + } + } catch (e: JSONException) { + Log.w("Pushe", "Failed to parse notification and convert it to Json.") + null + } + } + + @JvmStatic + fun getButtonsJsonArray(buttons: List): JSONArray? { + val jA = JSONArray() + for (i in buttons) { + jA.put(getButtonJsonObject(i)) + } + return jA + } + + ///// Background extensions + + @JvmStatic + fun getBackgroundNotificationObject(data: NotificationData, type: String, clickedButtonData: NotificationButtonData? = null): JSONObject? { + val finalObject = JSONObject() + val notificationObject = getNotificationJsonObject(data, clickedButtonData) + if (notificationObject == null) { + lg("Failed to get notification object") + return null + } + + finalObject.put("data", notificationObject) + + finalObject.put("type", type) + + return finalObject + } + + @JvmStatic + fun getCustomContent(json: Map): JSONObject { + val customContent = JSONObject() + customContent.put("json", JSONObject(json)) + customContent.put("type", Constants.CUSTOM_CONTENT) + return customContent + } + + @JvmStatic + fun getNotificationJsonFromIntent(intent: Intent): JSONObject { + val o = JSONObject() + try { + val data = intent.getStringExtra("data") + o.put("data", JSONObject(data)) + } catch (e: JSONException) { + lg("Failed to parse notification") + } + return o + } + + @JvmStatic + fun getCustomContentFromIntent(intent: Intent): JSONObject { + val o = JSONObject() + try { + val data = intent.getStringExtra("json") + o.put("json", JSONObject(data)) + } catch (e: JSONException) { + lg("Failed to parse notification") + } + return o + } + + @JvmStatic + fun getButtonJsonFromIntent(intent: Intent): JSONObject? { + var o: JSONObject? = JSONObject() + try { + val button = intent.getStringExtra("button") + o = JSONObject(button) + } catch (e: JSONException) { + lg("Failed to parse notification button") + } + return o + } + + @JvmStatic + fun getNotificationAndButtonFromIntent(intent: Intent): JSONObject { + val o = JSONObject() + try { + o.put("notification", getNotificationJsonFromIntent(intent)) + o.put("button", getButtonJsonFromIntent(intent)) + } catch (ignored: JSONException) { + } + return o + } +} + +object InAppUtils { + @JvmStatic + fun getInAppMessage(inAppMessage: InAppMessage): JSONObject { + val o = JSONObject() + try { + o.put("title", inAppMessage.title) + o.put("content", inAppMessage.content) + val buttons = JSONArray() + inAppMessage.buttons?.forEach { button -> + val btnObject = JSONObject() + btnObject.put("text", button.text) + buttons.put(btnObject) + } + o.put("buttons", buttons) + } catch (ignored: java.lang.Exception) { + lg("Failed to parse InAppMessage") + } + return o + } + + fun getInAppMessageFromIntent(intent: Intent): JSONObject? { + val o = JSONObject() + try { + val data = intent.getStringExtra("piam") + o.put("piam", JSONObject(data)) + } catch (ignored: java.lang.Exception) { + lg("Failed to retrieve message from broadcast.") + } + return o + } + + @JvmStatic + fun getInAppMessageAndButtonFromIntent(intent: Intent): JSONObject { + val o = JSONObject() + try { + o.put("message", getInAppMessageFromIntent(intent)) + o.put("index", intent.getStringExtra("index")) + } catch (ignored: JSONException) { + } + return o + } + +} + +/** + * Normal util class + * + */ +object Utils { + + /** + * Specifies whether the app is in the background or foreground. + */ + fun isAppOnForeground()= PusheLifeCycle.isForeground + + + /** + * Verbose logger. At first it is not enabled and it will be enabled using `PusheFlutterPlugin.setDebugMode(true)` + */ + @JvmStatic + fun lg(message: String) { + if (PusheFlutterPlugin.debugMode) { + Log.d("Pushe", message) + } + } +} + +/** + * Holding a static variable initialized in all lifecycle methods. + * If the activity comes up means the user is using and the app is in foreground, otherwise any method calling is happening in the background. + */ +internal object PusheLifeCycle { + var isForeground: Boolean = false +} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..07488ba --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..7fe65c8 --- /dev/null +++ b/example/README.md @@ -0,0 +1,8 @@ +# Pushe flutter sample + +Demonstrates how to use the pushe plugin. + +## Hint + +The app credentials belongs to Demo account at Pushe console. +Go to [Pushe console](https://console.pushe.co) and login to demo. You can send notifications to this app. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..e3afd8f --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,67 @@ +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 plugin: 'kotlin-android-extensions' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "co.pushe.pushesampleflutter" + minSdkVersion 17 + targetSdkVersion 29 + multiDexEnabled true + 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. + multiDexKeepProguard file('multidex-config.pro') + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'androidx.multidex:multidex:2.0.1' + testImplementation 'junit:junit:4.12' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/example/android/app/multidex-config.pro b/example/android/app/multidex-config.pro new file mode 100644 index 0000000..fc2d0b4 --- /dev/null +++ b/example/android/app/multidex-config.pro @@ -0,0 +1,4 @@ +-keep io.flutter.app.FlutterApplication { *; } +-keep io.flutter.view.FlutterMain { *; } +-keep io.flutter.util.PathUtils { *; } +-keep class android.app.** { *; } \ No newline at end of file diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..48db460 --- /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..0273f8a --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/co/pushe/pushesampleflutter/EmbeddingV1Activity.java b/example/android/app/src/main/java/co/pushe/pushesampleflutter/EmbeddingV1Activity.java new file mode 100644 index 0000000..15fb55f --- /dev/null +++ b/example/android/app/src/main/java/co/pushe/pushesampleflutter/EmbeddingV1Activity.java @@ -0,0 +1,13 @@ +package co.pushe.pushesampleflutter; + +import android.os.Bundle; + +import co.pushe.plus.flutter.PusheFlutterPlugin; +import io.flutter.app.FlutterActivity; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} \ No newline at end of file diff --git a/example/android/app/src/main/java/co/pushe/pushesampleflutter/MainActivity.java b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MainActivity.java new file mode 100644 index 0000000..7381ea0 --- /dev/null +++ b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MainActivity.java @@ -0,0 +1,8 @@ +package co.pushe.pushesampleflutter; + + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { + +} diff --git a/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyApp.java b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyApp.java new file mode 100644 index 0000000..1639c29 --- /dev/null +++ b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyApp.java @@ -0,0 +1,30 @@ +package co.pushe.pushesampleflutter; + +import android.content.Context; +import androidx.multidex.MultiDex; +import co.pushe.plus.flutter.PusheFlutterPlugin; +import io.flutter.app.FlutterApplication; +import io.flutter.plugin.common.PluginRegistry; + +import static co.pushe.plus.flutter.PusheFlutterPlugin.*; + +public class MyApp extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback { + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(this); + } + + @Override + public void onCreate() { + super.onCreate(); + setDebugMode(true); + initialize(this); + } + + @Override + public void registerWith(io.flutter.plugin.common.PluginRegistry registry) { + PusheFlutterPlugin.registerWith(registry); + } +} diff --git a/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyFCMService.java b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyFCMService.java new file mode 100644 index 0000000..e517770 --- /dev/null +++ b/example/android/app/src/main/java/co/pushe/pushesampleflutter/MyFCMService.java @@ -0,0 +1,37 @@ +package co.pushe.pushesampleflutter; + +// To make this work add firebase_messaging to your flutter app. + +//import com.google.firebase.messaging.RemoteMessage; +//import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService; + +@SuppressWarnings("SpellCheckingInspection") +public class MyFCMService /* extends FlutterFirebaseMessagingService*/ { +// @Override +// public void onNewToken(String s) { +// co.pushe.plus.Pushe.getFcmHandler(this).onNewToken(s); +// super.onNewToken(s); +// } +// @Override +// public void onDeletedMessages() { +// co.pushe.plus.Pushe.getFcmHandler(this).onDeletedMessages(); +// super.onDeletedMessages(); +// } +// @Override +// public void onSendError(String s, Exception e) { +// co.pushe.plus.Pushe.getFcmHandler(this).onSendError(s, e); +// super.onSendError(s, e); +// } +// @Override +// public void onMessageSent(String s) { +// co.pushe.plus.Pushe.getFcmHandler(this).onMessageSent(s); +// super.onMessageSent(s); +// } +// @Override +// public void onMessageReceived(RemoteMessage remoteMessage) { +// if (!co.pushe.plus.Pushe.getFcmHandler(this).onMessageReceived(remoteMessage)) { +// // It is for fire base, otherwise the condition will handle the message for Pushe +// super.onMessageReceived(remoteMessage); +// } +// } +} 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..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..48db460 --- /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..a8cb718 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.61' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.2' + 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..38c8d45 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=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..3291220 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 05 13:44:12 IRDT 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9367d48 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dfa1bca --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,514 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + 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 */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + 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 = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; 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 = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + 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 */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0910; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + 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 */, + 9740EEB41CF90195004384FC /* Debug.xcconfig 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\" 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"; + }; +/* 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; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + 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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = 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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + 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 = co.pushe.plusExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + 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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + 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_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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + 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 = co.pushe.plusExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.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 = co.pushe.plusExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.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/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..786d6aa --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..71cc41e --- /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: [UIApplicationLaunchOptionsKey: 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..3d43d11e66f4de3da27ed045ca4fe38ad8b48094 GIT binary patch literal 11112 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 P-1+_A+?hGkL;m(J7kc&K literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 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 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> 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..cbcfcba --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + pushe_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 + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..7335fdf --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..099e16d --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,18 @@ +import 'pushe_sample.dart'; +import 'package:flutter/material.dart'; + +void main() => runApp(PusheSampleApp()); + +class PusheSampleApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Pushe Flutter', + theme: ThemeData( + primarySwatch: Colors.indigo, + ), + home: PusheSampleWidget() + ); + } +} diff --git a/example/lib/pushe_sample.dart b/example/lib/pushe_sample.dart new file mode 100644 index 0000000..bbb434c --- /dev/null +++ b/example/lib/pushe_sample.dart @@ -0,0 +1,466 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:pushe_example/utils.dart'; +import 'package:pushe_flutter/pushe.dart'; + +/// The function is a top level which runs in another isolate. +/// This function must be top level or static, since it has to be independent of any class. +/// [message] is a map which has either 'data', 'json' or 'button' depending on event type. +/// if the type is 'receive', 'click' or 'dismiss', it will have data (which customContent is part of it). +/// if the type is 'customContent' it will have only 'json'. +/// if the type is 'buttonClick', it will contain 'data' and 'button'. +pusheBackgroundMessageHandler(String eventType, dynamic message) { + switch (eventType) { + case Pushe.notificationReceived: + var notificationData = NotificationData.fromDynamic(message); + print('Notification received in background $notificationData'); + break; + case Pushe.notifiactionClicked: + var notificationData = NotificationData.fromDynamic(message); + print('Notification clicked in background $notificationData'); + break; + case Pushe.notificationDismissed: + var notificationData = NotificationData.fromDynamic(message); + print('Notification dismissed in background $notificationData'); + break; + case Pushe.notificationButtonClicked: + var notificationData = NotificationData.fromDynamic(message); + print( + 'Notification button clicked in background $notificationData & clicked button is ${notificationData.clickedButton}'); + break; + case Pushe.customContentReceived: + // Message is a map and can not be parsed + print('Custom content received in background $message'); + break; + } +} + +class PusheSampleWidget extends StatefulWidget { + createState() => _PusheSampleState(); +} + +class _PusheSampleState extends State { + // Fields + + String statusText = ""; + var _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _implementListeners(); + } + + void _updateStatus(String text) async { + setState(() { + statusText = '$statusText \n \n $text \n ${DateTime.now()}'; + }); + } + + void _implementListeners() { + Pushe.setNotificationListener( + onReceived: (notificationData) => + _updateStatus('Notification received: $notificationData'), + onClicked: (notificationData) => + _updateStatus('Notification clicked: $notificationData'), + onDismissed: (notificationData) => + _updateStatus('Notification dismissed: $notificationData'), + onButtonClicked: (notificationData) => _updateStatus( + 'Notification button clicked: $notificationData, ${notificationData.clickedButton}'), + onCustomContentReceived: (customContent) => _updateStatus( + 'Notification custom content received: $customContent'), + // This function only works in background + onBackgroundNotificationReceived: pusheBackgroundMessageHandler); + Pushe.setInAppMessagingListener(onReceived: (inAppMessage) { + _updateStatus( + '[InApp was received]\nTitle:${inAppMessage.title}\nContent:${inAppMessage.content}'); + }, onTriggered: (inAppMessage) { + _updateStatus( + '[InApp was triggered]\nTitle:${inAppMessage.title}\nContent:${inAppMessage.content}'); + }, onClicked: (inAppMessage) { + _updateStatus( + '[InApp was clicked]\nTitle:${inAppMessage.title}\nContent:${inAppMessage.content}'); + }, onDismissed: (inAppMessage) { + _updateStatus( + '[InApp was dismissed]\nTitle:${inAppMessage.title}\nContent:${inAppMessage.content}'); + }, onButtonClicked: (inAppMessage, buttonIndex) { + _updateStatus( + '[InApp button was clicked]\nTitle:${inAppMessage.title}\nContent:${inAppMessage.content}\nButton: ${inAppMessage.buttons[buttonIndex].text}'); + }); + } + + void _clearStatus() { + setState(() { + statusText = ""; + _scrollController.animateTo( + 0.0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + }); + } + + // All managing functions + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Pushe flutter'), + centerTitle: true, + bottom: PreferredSize( + child: Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 2), + child: Text('Flutter plugin: 2.2.1', + style: TextStyle(color: Colors.white)), + ), + preferredSize: null), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: SingleChildScrollView( + child: _getList(_getActions()), + ), + flex: 8, + ), + Flexible( + child: Divider( + height: 16.0, + color: Colors.blueGrey, + ), + flex: 1, + ), + Flexible( + child: SingleChildScrollView( + controller: _scrollController, + reverse: true, + child: Container( + decoration: BoxDecoration(color: Colors.white), + child: Padding( + padding: EdgeInsets.all(4.0), + child: GestureDetector( + onDoubleTap: _clearStatus, + child: Text(statusText, + style: TextStyle(color: Colors.blueGrey))))), + ), + flex: 6, + ) + ], + )); + } + + Widget _getList(Map values) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: values.keys.map((itemText) { + return Padding( + padding: EdgeInsets.all(4.0), + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + child: InkWell( + onTap: () => values[itemText](), // call it's action + child: Card( + elevation: 2, + margin: EdgeInsets.all(2.0), + child: Center( + child: Text(itemText, + style: TextStyle( + color: Colors.indigo, + fontWeight: FontWeight.bold, + fontSize: 15.0)))), + )), + ); + }).toList(), + ); + } + + /// + /// All possible actions which come into the list + /// + Map _getActions() { + return { + "IDs": () async { + alert(context, () {}, + title: 'IDs', + message: + "DeviceId:\n${await Pushe.getDeviceId()}\n\nGoogleAdId:\n${await Pushe.getGoogleAdvertisingId()}"); + }, + "Custom ID": () async { + await getInfo(context, (text) { + Pushe.setCustomId(text); + _updateStatus('CustomId is $text'); + }, + title: 'New customId', + message: 'Current customId: ${await Pushe.getCustomId()}'); + }, + "PhoneNumber": () async { + await getInfo(context, (text) { + Pushe.setUserPhoneNumber(text); + _updateStatus('PhoneNumber is $text'); + }, + title: 'New PhoneNumber', + message: + 'Current PhoneNumber: ${await Pushe.getUserPhoneNumber()}'); + }, + "Email": () async { + await getInfo(context, (text) { + Pushe.setUserEmail(text); + _updateStatus('Email is $text'); + }, + title: 'New Email', + message: 'Current Email: ${await Pushe.getUserEmail()}'); + }, + "Module intiatization status": () async { + _updateStatus('Modules initialzed: ${await Pushe.isInitialized()}'); + }, + "Module Registration status": () async { + _updateStatus('Device Registered: ${await Pushe.isRegistered()}'); + }, + "Topic": () async { + await getInfo( + context, + (text) { + Pushe.subscribe(text, callback: () { + _updateStatus('Subscribed to $text'); + }); + }, + title: 'Topic', + message: """ +Topics: ${(await Pushe.getSubscribedTopics()).toString()}\n +Enter topic name to subscribe or unsubscribe: + """, + positive: 'Subscribe', + negative: 'Unsubscribe', + onNegative: (text) { + Pushe.unsubscribe(text, callback: () { + _updateStatus('Unsubscribed from $text'); + }); + }); + }, + "Notification channel": () async { + await getInfo( + context, + (text) { + // Create (only name and Id) + var parts = text.split(":"); + if (parts.length != 2) { + _updateStatus("Enter id and name in id:name format"); + return; + } + + var id = parts[0]; + var name = parts[1]; + Pushe.createNotificationChannel(id, name); + _updateStatus("Create notification channel $name"); + }, + title: "Channel", + message: + 'Enter channel id and name in id:name format and tap create to create a channel\nOr enter channel id and tap remove to remove a channel', + positive: 'Create', + negative: "Remove", + onNegative: (text) { + var id = text; + Pushe.removeNotificationChannel(id); + _updateStatus('Remove notification channel with id $id'); + }); + }, + "Tag (name:value)": () async { + await getInfo( + context, + (text) { + var parts = text.split(":"); + if (parts.length != 2) return; + Pushe.addTags({parts[0]: parts[1]}, callback: () { + _updateStatus('Tag ${parts[0]} added'); + }); + }, + title: 'Tag', + message: """ + Tags: + ${(await Pushe.getSubscribedTags()).toString()} + Tag in name:value format (add) + Tag in name1,name2 format (remove) + """, + positive: 'Add', + negative: 'Remove', + onNegative: (text) { + var parts = text.split(","); + if (parts == null || parts.isEmpty) return; + Pushe.removeTags(parts, callback: () { + _updateStatus('Tags $parts removed'); + }); + }); + }, + "Analytics: Event": () async { + await getInfo(context, (text) { + Pushe.sendEvent(text); + _updateStatus('Sending event: $text'); + }, title: 'Event', message: 'Type event name to send'); + }, + "Analytics: Ecommerce": () async { + await getInfo(context, (text) { + var parts = text.split(":"); + if (parts.length != 2) return; + try { + Pushe.sendEcommerceData(parts[0], double.parse(parts[1])); + _updateStatus( + 'Sending Ecommerce data with name ${parts[0]} and price ${parts[1]}'); + } catch (e) { + _updateStatus('Enter valid price (price is double)'); + } + }, + title: 'Ecommerce', + message: 'Enter value in name:price format to send data'); + }, + "Notification: DeviceId": () async { + await getInfo( + context, + (text) async { + Pushe.sendNotificationToUser(IdType.DeviceId, + await Pushe.getDeviceId(), 'Title for me', 'Content for me'); + _updateStatus('Sending notification to this device'); + }, + title: 'Notification', + message: + 'Enter androidId to send a simple notification to the user', + positive: 'Send to me', + negative: 'Send to ...', + onNegative: (text) { + Pushe.sendNotificationToUser( + IdType.DeviceId, text, 'Test title', 'Test content'); + _updateStatus('Sending notification to AndroidId: $text'); + }); + }, + "Notification: GoogleAdId": () async { + await getInfo( + context, + (text) async { + Pushe.sendNotificationToUser( + IdType.GoogleAdvertisingId, + await Pushe.getGoogleAdvertisingId(), + 'Title for me', + 'Content for me'); + _updateStatus('Sending notification to this device'); + }, + title: 'Notification', + message: + 'Enter GoogleAdID to send a simple notification to the user', + positive: 'Send to me', + negative: 'Send to ...', + onNegative: (text) { + Pushe.sendNotificationToUser(IdType.GoogleAdvertisingId, text, + 'Test title', 'Test content'); + _updateStatus('Sending notification to GoogleAdID: $text'); + }); + }, + "Notification: CustomId": () async { + await getInfo( + context, + (text) { + Pushe.getCustomId().then((value) { + if (value == null || value.isEmpty) { + _updateStatus("Can not send by CustomID when there's none"); + return; + } + Pushe.sendNotificationToUser( + IdType.CustomId, value, 'Title for me', 'Content for me'); + _updateStatus('Sending notification to this device'); + }); + }, + title: 'Notification', + message: 'Enter CustomId to send a simple notification to the user', + positive: 'Send to me', + negative: 'Send to ...', + onNegative: (text) { + Pushe.sendNotificationToUser( + IdType.CustomId, text, 'Test title', 'Test content'); + _updateStatus('Sending notification to CustomId: $text'); + }); + }, + "Enable/Disable notification": () async { + await alert( + context, + () { + Pushe.setNotificationOn(); + _updateStatus("Notifications will be shown"); + }, + title: 'Notification', + message: + 'Current status: ${await Pushe.isNotificationOn() ? "Enabled" : "Disabled"}\nDo you want to enable or disable notification publishing?', + ok: 'Enable', + no: 'Disable', + onNo: () { + Pushe.setNotificationOff(); + _updateStatus("Notifications won't be shown if received"); + }); + }, + "Enable/Disable custom sound": () async { + await alert( + context, + () { + Pushe.enableCustomSound(); + _updateStatus('Custom sound will be played if received'); + }, + title: 'Custom sound', + message: + 'Current status: ${await Pushe.isCustomSoundEnabled() ? "Enabled" : "Disabled"}\nDo you want to enable or disable custom sound for notification?', + ok: 'Enable', + no: 'Disable', + onNo: () { + Pushe.disableCustomSound(); + _updateStatus("Custom sound won't be played if received"); + }); + }, + "InApp: trigger event": () async { + getInfo(context, (value) async { + if (value.isNotEmpty) { + _updateStatus('Triggering local event $value'); + await Pushe.triggerEvent(value); + _updateStatus('Event triggered'); + } + }, title: 'InApp event', message: 'Enter name of event'); + }, + "InApp: Toggle in app display": () async { + if (await Pushe.isInAppMessagingEnabled()) { + _updateStatus('Disabling InAppMessaging'); + await Pushe.disableInAppMessaging(); + _updateStatus('InApp display is OFF'); + } else { + _updateStatus('Enabling InAppMessaging'); + await Pushe.enableInAppMessaging(); + _updateStatus('InApp display is ON'); + } + }, + "InApp: dismiss shown InApp message": () async { + Pushe.dismissShownInApp(); + }, + "InApp: Send test message (Test)": () async { + getInfo( + context, + (value) { + // ignore: invalid_use_of_visible_for_testing_member + Pushe.testInAppMessage(value, instant: false); + }, + title: 'TEST: InApp', + message: + 'Enter the desired message\nInstant: It will not wait for trigger and just shows it\nNotInstant: Exactly behaves as a real message', + positive: 'Not instant', + negative: 'Instant', + onNegative: (value) async { + // ignore: invalid_use_of_visible_for_testing_member + Pushe.testInAppMessage(value, instant: true); + } + ); + }, + "ReImplement listeners": () { + _updateStatus('Listeners re-implemented'); + _implementListeners(); + } + }; + } +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart new file mode 100644 index 0000000..f090664 --- /dev/null +++ b/example/lib/utils.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +Future alert(BuildContext context, Function onOK, + {String title: 'Pushe', String message: 'Do you accept?', String ok: 'OK', String no: 'Cancel', Function onNo}) async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text(message), + ], + ), + ), + actions: [ + FlatButton( + child: Text(ok), + onPressed: () { + onOK(); + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text(no), + onPressed: () { + onNo?.call(); + Navigator.of(context).pop(); + }, + ) + ], + ); + }, + ); +} + +Future getInfo(BuildContext context, Function(String) onPositive, + {String title: 'Pushe', + String message: 'Do you accept?', + String positive: 'OK', + String negative: 'Cancel', + Function(String) onNegative}) async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + var result = ""; + return AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text(message), + TextFormField( + decoration: InputDecoration(hintText: title), + onChanged: (text) { + result = text; + }, + ), + ], + ), + ), + actions: [ + FlatButton( + child: Text(positive), + onPressed: () async { + Navigator.of(context).pop(); + await onPositive(result); + }, + ), + FlatButton( + child: Text(negative), + onPressed: () async { + Navigator.of(context).pop(); + await onNegative?.call(result); + }, + ) + ], + ); + }, + ); +} \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..c4289be --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,161 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0-nullsafety.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0-nullsafety.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10-nullsafety.1" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0-nullsafety.1" + pushe_flutter: + dependency: "direct dev" + description: + path: ".." + relative: true + source: path + version: "2.4.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0-nullsafety.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0-nullsafety.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.19-nullsafety.2" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.3" +sdks: + dart: ">=2.10.0-110 <2.11.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..0075933 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,64 @@ +name: pushe_example +description: Demonstrates how to use the pushe plugin. +publish_to: 'none' +version: 2.1.0+2109 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + pushe_flutter: + path: ../ + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/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..77a1d6d --- /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:pushe_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(PusheSampleApp()); + + // 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/PusheFlutterPlugin.h b/ios/Classes/PusheFlutterPlugin.h new file mode 100644 index 0000000..44b1e74 --- /dev/null +++ b/ios/Classes/PusheFlutterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface PusheFlutterPlugin : NSObject +@end diff --git a/ios/Classes/PusheFlutterPlugin.m b/ios/Classes/PusheFlutterPlugin.m new file mode 100644 index 0000000..32b4e8e --- /dev/null +++ b/ios/Classes/PusheFlutterPlugin.m @@ -0,0 +1,15 @@ +#import "PusheFlutterPlugin.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 "pushe_flutter-Swift.h" +#endif + +@implementation PusheFlutterPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftPusheFlutterPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/SwiftPusheFlutterPlugin.swift b/ios/Classes/SwiftPusheFlutterPlugin.swift new file mode 100644 index 0000000..87d6b99 --- /dev/null +++ b/ios/Classes/SwiftPusheFlutterPlugin.swift @@ -0,0 +1,92 @@ +import Flutter +import UIKit +import Pushe + +public class SwiftPusheFlutterPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "plus.pushe.co/pushe_flutter", binaryMessenger: registrar.messenger()) + let instance = SwiftPusheFlutterPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + print("<\(call.method)> requested") + switch call.method { + case "Pushe.initialize": + PusheClient.shared.initialize() + case "Pushe.getDeviceId": + result(PusheClient.shared.getDeviceId()) + case "Pushe.getAdvertisingId": + result(PusheClient.shared.getAdvertisingId()) + case "Pushe.setCustomId": + let arguments = call.arguments as? [String: Any] + let id = arguments?["id"] as? String + PusheClient.shared.setCustomId(id: id) + case "Pushe.getCustomId": + result(PusheClient.shared.getCustomId()) + case "Pushe.setUserEmail": + let arguments = call.arguments as? [String: Any] + let email = arguments?["email"] as? String + let _ = PusheClient.shared.setUserEmail(email: email) + case "Pushe.getUserEmail": + result(PusheClient.shared.getUserEmail()) + case "Pushe.setUserPhoneNumber": + let arguments = call.arguments as? [String: Any] + let phoneNumber = arguments?["phone"] as? String + let _ = PusheClient.shared.setUserPhoneNumber(phoneNumber: phoneNumber) + case "Pushe.getUserPhoneNumber": + result(PusheClient.shared.getUserPhoneNumber()) + case "Pushe.addTags": + guard let arguments = call.arguments as? [String: Any], + let tags = arguments["tags"] as? [String: String] else { break } + + PusheClient.shared.addTags(with: tags) + case "Pushe.removeTags": + guard let arguments = call.arguments as? [String: Any], + let tags = arguments["tags"] as? [String] else { break } + + PusheClient.shared.removeTags(with: tags) + case "Pushe.getSubscribedTags": + result(PusheClient.shared.getSubscribedTags()) + case "Pushe.subscribe": + guard let arguments = call.arguments as? [String: Any], + let topic = arguments["topic"] as? String else { break } + + PusheClient.shared.subscribe(to: topic) + case "Pushe.unsubscribe": + guard let arguments = call.arguments as? [String: Any], + let topic = arguments["topic"] as? String else { break } + + PusheClient.shared.unsubscribe(from: topic) + case "Pushe.getSubscribedTopics": + result(PusheClient.shared.getSubscribedTopics()) + case "Pushe.isInitialized", "Pushe.isRegistered": + result(PusheClient.shared.isRegistered()) + case "Pushe.sendEvent": + guard let arguments = call.arguments as? [String: Any], + let eventName = arguments["name"] as? String else { break } + + let action: EventAction + switch arguments["action"] as? String { + case "EventAction.sign_up": + action = EventAction.signUp + case "EventAction.login": + action = EventAction.login + case "EventAction.purchase": + action = EventAction.purchase + case "EventAction.achievement": + action = EventAction.achievement + case "EventAction.level": + action = EventAction.level + default: + action = EventAction.custom + } + + let data = arguments["data"] as? [String: Any] + PusheClient.shared.sendEvent(event: Event(name: eventName, action: action, data: data)) + default: + result("requested method <\(call.method)> not implemented") + } + } +} + diff --git a/ios/pushe_flutter.podspec b/ios/pushe_flutter.podspec new file mode 100644 index 0000000..f8fd464 --- /dev/null +++ b/ios/pushe_flutter.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint pushe_flutter.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'pushe_flutter' + s.version = '2.2.1' + s.summary = 'Flutter plugin for Pushe sdk' + s.description = <<-DESC + Pushe is a framework written in swift, helping to receive push-notifications in iOS devices. + DESC + s.homepage = 'https://pushe.co' + s.license = { :file => '../LICENSE' } + s.author = { 'Jafar Khoshtabiat' => 'jafar.khoshtabiat@pushe.co' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'Pushe', '1.0.16' + # s.platform = :ios, '8.0' + s.ios.deployment_target = '10.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' + s.static_framework = true +end diff --git a/lib/pushe.dart b/lib/pushe.dart new file mode 100644 index 0000000..0d664df --- /dev/null +++ b/lib/pushe.dart @@ -0,0 +1,668 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// +/// Enum: Notification id type +/// @author Mahdi Malvandi +/// +enum IdType { DeviceId, GoogleAdvertisingId, CustomId } +enum EventAction { custom, sign_up, login, purchase, achievement, level } + +void _pusheSetupBackgroundChannel() async { + MethodChannel backgroundChannel = + const MethodChannel('plus.pushe.co/pushe_flutter_background'); + // Setup Flutter state needed for MethodChannels. + WidgetsFlutterBinding.ensureInitialized(); + + // This is where the magic happens and we handle background events from the + // native portion of the plugin. + backgroundChannel.setMethodCallHandler((MethodCall call) async { + if (call.method == 'handleBackgroundMessage') { + final CallbackHandle handle = + CallbackHandle.fromRawHandle(call.arguments['handle']); + final Function handlerFunction = + PluginUtilities.getCallbackFromHandle(handle); + try { + var dataArg = call.arguments['message']; + if (dataArg == null) { + return; + } + Map wholeData = jsonDecode(dataArg); + String eventType = wholeData['type']; + Map messageContent = eventType == "custom_content" + ? wholeData['json'] + : wholeData['data']; + + await handlerFunction(eventType, messageContent); + } catch (e) { + print('Pushe: Unable to handle incoming background message.\n$e'); + } + } + return Future.value(); + }); + + // Once we've finished initializing, let the native portion of the plugin + // know that it can start scheduling handling messages. + backgroundChannel.invokeMethod('Pushe.platformInitialized'); +} + +/// +/// @author Mahdi Malvandi +/// Main plugin class handling most of SDK's works. +/// +class Pushe { + // Static fields + static const String notificationReceived = 'receive'; + static const String notifiactionClicked = 'click'; + static const String notificationDismissed = 'dismiss'; + static const String notificationButtonClicked = 'button_click'; + static const String customContentReceived = 'custom_content'; + + // Notification Callback handlers + static void Function(NotificationData) _receiveCallback; + static void Function(NotificationData) _clickCallback; + static void Function(NotificationData) _dismissCallback; + static void Function(dynamic) _customContentCallback; + static void Function(NotificationData) _buttonClickCallback; + + // InAppMessage callback handlers + static void Function(InAppMessage) _inAppReceiveCallback; + static void Function(InAppMessage) _inAppTriggerCallback; + static void Function(InAppMessage) _inAppClickCallback; + static void Function(InAppMessage) _inAppDismissCallback; + static void Function(InAppMessage, int) _inAppButtonClickCallback; + + static const MethodChannel _channel = + const MethodChannel('plus.pushe.co/pushe_flutter'); + + /// GDPR related + /// You can use this function after getting user consent to initialize pushe lib + /// for ios only: you must call this function in order to pushe to start working + static Future initialize() async => + await _channel.invokeMethod("Pushe.initialize"); + + /// GDPR related + /// If user gave consent for collecting necessary extra data for Analytics. + /// Simply call this function with True parameter + /// the related task will be scheduled right away and will collect when needed. + /// If for any reason user decided to undo his dialog action, or for some other reason, you wanted to cancel consent, + /// Simply call this function with False parameter + /// [enabled] enable or disable user consent for collecting extra data + /// + static Future setUserConsentGiven(bool enabled) async => await _channel + .invokeMethod("Pushe.setUserConsentGiven", {"enabled": enabled}); + + /// Get the user consent status + static Future getUserConsentStatus() async => + await _channel.invokeMethod("Pushe.getUserConsentStatus"); + + /// Get the unique id of the devices + static Future getDeviceId() async => + await _channel.invokeMethod("Pushe.getDeviceId"); + + @Deprecated('Use `getDeviceId` instead') + static Future getAndroidId() async => + await _channel.invokeMethod("Pushe.getAndroidId"); + + // android only + /// Get google advertising id + static Future getGoogleAdvertisingId() async => + await _channel.invokeMethod("Pushe.getGoogleAdvertisingId"); + + static Future getAdvertisingId() async => + await _channel.invokeMethod("Pushe.getAdvertisingId"); + + /// Get custom id + static Future getCustomId() async => + await _channel.invokeMethod("Pushe.getCustomId"); + + /// Set custom id + static Future setCustomId(String id) async => + await _channel.invokeMethod("Pushe.setCustomId", {"id": id}); + + /// Get email + static Future getUserEmail() async => + await _channel.invokeMethod("Pushe.getUserEmail"); + + /// Set email + static Future setUserEmail(String email) async => + await _channel.invokeMethod("Pushe.setUserEmail", {"email": email}); + + /// Get user phone number + static Future getUserPhoneNumber() async => + await _channel.invokeMethod("Pushe.getUserPhoneNumber"); + + /// Set user phone number + static Future setUserPhoneNumber(String phone) async => + await _channel.invokeMethod("Pushe.setUserPhoneNumber", {"phone": phone}); + + /// Add tags. + /// [tags] key-value pairs + /// [callback] is an optional function that will be called with result of adding tags. + static Future addTags(Map tags, + {Function callback}) async { + if (await _channel.invokeMethod("Pushe.addTags", {"tags": tags})) { + callback?.call(); + } + return; + } + + /// Remove tags. + /// [tags] list of tag keys to remove + /// [callback] is an optional function that will be called with result of removing tags. + static Future removeTags(List tags, {Function callback}) async { + if (await _channel.invokeMethod("Pushe.removeTags", {"tags": tags})) { + callback?.call(); + } + return; + } + + /// Get subscribed tags + static Future getSubscribedTags() async => + await _channel.invokeMethod("Pushe.getSubscribedTags"); + + /// Get subscribed topics + static Future getSubscribedTopics() async => + await _channel.invokeMethod("Pushe.getSubscribedTopics"); + + /// Subscribe to a topic. + /// [topic] is the name of that topic. The naming rules must follow FCM topic naming standards. + /// [callback] is an optional function that will be called with result of subscription. + static Future subscribe(String topic, {Function callback}) async { + if (await _channel.invokeMethod("Pushe.subscribe", {"topic": topic})) { + callback?.call(); + } + return; + } + + /// Unsubscribe from a topic already subscribed. + /// [topic] is the name of that topic. The naming rules must follow FCM topic naming standards. + /// [callback] is an optional function that will be called with result of Unsubscription. + static Future unsubscribe(String topic, {Function callback}) async { + if (await _channel.invokeMethod("Pushe.unsubscribe", {"topic": topic})) { + callback?.call(); + } + return; + } + + /// If this function is called, notification will not be shown. + static Future setNotificationOff() async => + await _channel.invokeMethod("Pushe.disableNotifications"); + + /// Default of notification is set to On, if you have set it off, you can revert it using this function. + static Future setNotificationOn() async => + await _channel.invokeMethod("Pushe.enableNotifications"); + + /// To check whether notification will publish or not (default is true of course) + static Future isNotificationOn() async => + await _channel.invokeMethod("Pushe.isNotificationEnable"); + + /// Custom sound is enabled by default. If you wanted to disable it and use the default sound (like for avoiding downloading sound) you can disable it. + static Future disableCustomSound() async => + await _channel.invokeMethod("Pushe.disableCustomSound"); + + /// Custom sound is enabled by default. However, if you have turned it off, you can revert it using this function + static Future enableCustomSound() async => + await _channel.invokeMethod("Pushe.enableCustomSound"); + + /// To check whether custom sound is already enabled or not + static Future isCustomSoundEnabled() async => + await _channel.invokeMethod("Pushe.isCustomSoundEnabled"); + + // region InAppMessage + + static Future triggerEvent(String eventName) async => + await _channel.invokeMethod("Pushe.triggerEvent", {'event': eventName}); + + static Future disableInAppMessaging() async => + await _channel.invokeMethod("Pushe.disableInAppMessaging"); + + static Future enableInAppMessaging() async => + await _channel.invokeMethod("Pushe.enableInAppMessaging"); + + static Future isInAppMessagingEnabled() async => + await _channel.invokeMethod("Pushe.isInAppMessagingEnabled"); + + static Future dismissShownInApp() async => + await _channel.invokeMethod("Pushe.dismissShownInApp"); + + @visibleForTesting + static Future testInAppMessage(String message, + {bool instant = false}) async => + await _channel.invokeMethod( + "Pushe.testInAppMessage", {'message': message, 'instant': instant}); + + static setInAppMessagingListener({ + Function(InAppMessage) onReceived, + Function(InAppMessage) onTriggered, + Function(InAppMessage) onClicked, + Function(InAppMessage) onDismissed, + Function(InAppMessage, int) onButtonClicked, + }) async { + _inAppReceiveCallback = onReceived; + _inAppClickCallback = onClicked; + _inAppDismissCallback = onDismissed; + _inAppTriggerCallback = onTriggered; + _inAppButtonClickCallback = onButtonClicked; + await _channel.invokeMethod("Pushe.initializeInAppListeners"); + } + + // endregion + + /// Creates a channel using native API. Read more about channel at https://developer.android.com/training/notify-user/channels + /// You can send notifications through only one channel, so users that have created that channel in their devices, can receive it, + /// while rest of them can not. + /// For android versions lower than 8.0 (API 26), this method has no function + static Future createNotificationChannel( + String channelId, String channelName, + {String description, + int importance, + bool enableLight, + bool enableVibration, + bool showBadge, + int ledColor, + List vibrationPattern}) async { + var args = {}; + args['channelId'] = channelId; + args['channelName'] = channelName; + args['description'] = description; + args['importance'] = importance; + args['enableLight'] = enableLight; + args['enableVibration'] = enableVibration; + args['showBadge'] = showBadge; + args['ledColor'] = ledColor; + args['vibrationPattern'] = vibrationPattern; + + return _channel.invokeMethod("Pushe.createNotificationChannel", args); + } + + /// Remove the channel Id that was created + /// For android versions lower than 8.0 (API 26), this method has no function + static Future removeNotificationChannel(String channelId) { + return _channel.invokeMethod( + "Pushe.removeNotificationChannel", {"channelId": channelId}); + } + + /// Check if Pushe is initialized to server or not. + static Future isInitialized() async => + await _channel.invokeMethod("Pushe.isInitialized"); + + /// Check if Pushe is registered to server or not. + static Future isRegistered() async => + await _channel.invokeMethod("Pushe.isRegistered"); + + /// Call it's callback when registration is completed + static Future setRegistrationCompleteListener(Function callback) async { + var result = + await _channel.invokeMethod("Pushe.setRegistrationCompleteListener"); + if (result) callback?.call(); + return; + } + + /// Call it's callback when initialization is completed + static Future setInitializationCompleteListener( + Function callback) async { + var result = + await _channel.invokeMethod("Pushe.setInitializationCompleteListener"); + if (result) callback?.call(); + return; + } + + /// + /// Sending notification from this device to another device which is registered as a user of this app. + /// [type] is the type of unique id which you are passing + /// [id] is the id of the type [type] + /// [title] is the title of the notification + /// [content] is the content of the notification + /// [bigTitle] is the complete title of the notification + /// [bigContent] is the complete content of the notification + /// [imageUrl] is the url of the image which notification can contain + /// [iconUrl] is the url of the notification icon + /// [customContent] is the custom json you send along with the notification message which can be received as the notification customContent in the target device + static Future sendNotificationToUser( + IdType type, String id, String title, String content, + {String bigTitle, + String bigContent, + String imageUrl, + String iconUrl, + String notificationIcon, + dynamic customContent}) async { + String idType = type.toString(); + await _channel.invokeMethod('Pushe.sendUserNotification', { + "type": idType, + "id": id, + "title": title, + "content": content, + "bigTitle": bigTitle, + "bigContent": bigContent, + "imageUrl": imageUrl, + "iconUrl": iconUrl, + "notifIcon": notificationIcon, + "customContent": jsonEncode(customContent) + }); + return; + } + + static Future sendAdvancedNotificationToUser( + IdType type, String id, String json) async { + String idType = type.toString(); + await _channel.invokeMethod('Pushe.sendAdvancedUserNotification', + {"type": idType, "id": id, "advancedJson": jsonEncode(json)}); + return; + } + + ///Send an event + ///[name] is the name of event that wants to send + /// Possible option: SendEvent + static Future sendEvent(String name, + {EventAction action: EventAction.custom, dynamic data}) async { + var arguments = {}; + arguments['name'] = name; + arguments['action'] = action.toString(); + if (data != null) { + arguments['data'] = data; + } + + return await _channel.invokeMethod("Pushe.sendEvent", arguments); + } + + /// Send ecommerce data. + /// [name] is the name of ecommerce data + /// [price] is the value of ecommerce name + static Future sendEcommerceData(String name, double price, + {String category, int quantity}) async { + var args = {}; + args['name'] = name; + args['price'] = price; + if (category != null) { + args['category'] = category; + } + if (quantity != null) { + args['quantity'] = quantity; + } + return await _channel.invokeMethod("Pushe.sendEcommerceData", args); + } + + /// Set callbacks for different types of events for notifications (in foreground or when app is open in the background) + /// [onReceived] is called when notification was received. + /// [onClicked] is called when notification was clicked. + /// [onDismissed] is called when notification was swiped away. + /// [onButtonClicked] is called when notification contains button and a button was clicked. + /// [onCustomContentReceived] is called when notification includes custom json. It will a json in string format. + /// [onBackgroundNotificationReceived] is the function that will be called when notification is received in the background, + /// this would be a **TopLevel** or **Static** function will be passed and saved for later usage. + /// Function will take a dynamic value which is a dictionary (aka Map), which contains 'type' (one of receive, click, button_click, custom_content, dismiss), + /// and the 'data' either a custom map (provided as customContent), the notification, or the notification and the button (in case of button click) + /// In addition, the Isolate of the top level function is different and it will not have access to any widget (if app is in foreground, this does not get called and instead + /// foreground function will take place). So be aware of the isolate difference. + static Future setNotificationListener({ + Function(NotificationData) onReceived, + Function(NotificationData) onClicked, + Function(NotificationData) onDismissed, + Function(NotificationData) onButtonClicked, + Function(dynamic) onCustomContentReceived, + Function(String, dynamic) onBackgroundNotificationReceived, + }) async { + _receiveCallback = onReceived; + _clickCallback = onClicked; + _dismissCallback = onDismissed; + _buttonClickCallback = onButtonClicked; + _customContentCallback = onCustomContentReceived; + _channel.setMethodCallHandler(_handleMethod); + + // In case that application was not overrode and background was not used, this will be helpful (sets the listener after app is started which will not trigger background) + _channel.invokeMethod("Pushe.initNotificationListenerManually"); + if (onBackgroundNotificationReceived != null) { + // If background was set, get the handles to save + final CallbackHandle backgroundSetupHandle = + PluginUtilities.getCallbackHandle(_pusheSetupBackgroundChannel); + final CallbackHandle backgroundMessageHandle = + PluginUtilities.getCallbackHandle(onBackgroundNotificationReceived); + // Callback must be exactly a top level or static and should be dependent to any inner scopes + if (backgroundMessageHandle == null) { + throw ArgumentError( + '''Failed to setup background handle. `backgroundNotificationListener` must be a TOPLEVEL or a STATIC method. + Checkout Flutter FAQ at https://docs.pushe.co for more information. + '''); + } + _channel.invokeMethod( + 'Pushe.notificationListener', + { + 'setupHandle': backgroundSetupHandle.toRawHandle(), + 'backgroundHandle': backgroundMessageHandle.toRawHandle() + }, + ); + } + return; + } + + /// + /// For foreground notification, a broadcast receiver will send the stuff using channel to dart side, + /// thus, the channel in dart side must handle the messages which are received. This function will handle this job. + /// If a method was called from native code through channel this will handle it. + /// + static Future _handleMethod(MethodCall call) async { + dynamic arg = jsonDecode(call.arguments); + var callMethod = call.method; + switch (callMethod) { + case 'Pushe.onNotificationReceived': + _receiveCallback?.call(NotificationData.fromDynamic(arg['data'])); + break; + case 'Pushe.onNotificationClicked': + _clickCallback?.call(NotificationData.fromDynamic(arg['data'])); + break; + case 'Pushe.onNotificationButtonClicked': + try { + _buttonClickCallback?.call(NotificationData.fromDynamic(arg['data'])); + } catch (e) { + print( + 'Pushe: Error passing notification data to callback ${e.toString()}'); + } + break; + case 'Pushe.onCustomContentReceived': + try { + var customContent = arg['json']; + _customContentCallback?.call(customContent); + } catch (e) { + print('Pushe: Error passing customContent to callback'); + } + break; + case 'Pushe.onNotificationDismissed': + _dismissCallback?.call(NotificationData.fromDynamic(arg['data'])); + break; + // InAppMessaging + case 'Pushe.inAppMessageReceived': + _inAppReceiveCallback?.call(InAppMessage.fromDynamic(arg['piam'])); + break; + case 'Pushe.inAppMessageTriggered': + _inAppTriggerCallback?.call(InAppMessage.fromDynamic(arg['piam'])); + break; + case 'Pushe.inAppMessageClicked': + _inAppClickCallback?.call(InAppMessage.fromDynamic(arg['piam'])); + break; + case 'Pushe.inAppMessageButtonClicked': + _inAppButtonClickCallback?.call( + InAppMessage.fromDynamic(arg['message']['piam']), + int.parse(arg['index'], onError: (_) => -1), + ); + break; + case 'Pushe.inAppMessageDismissed': + _inAppDismissCallback?.call(InAppMessage.fromDynamic(arg['piam'])); + break; + } + + return null; + } +} + +// region Notification + +/// +/// Notification data class as an interface between native callback data classes and Flutter dart code. +/// When a notification event happens (like Receive), callbacks will hold instances of this class. +/// +class NotificationData { + String _title, + _content, + _bigTitle, + _bigContent, + _summary, + _imageUrl, + _iconUrl; + dynamic _customContent; + List _buttons; + NotificationButtonData _clickedButton; + + NotificationData.create( + this._title, + this._content, + this._bigTitle, + this._bigContent, + this._summary, + this._imageUrl, + this._iconUrl, + this._customContent, + this._buttons, + this._clickedButton); + + static NotificationData fromDynamic(dynamic data) { + try { + List notificationButtons; + try { + notificationButtons = NotificationButtonData.fromList(data['buttons']); + } catch (e) { + notificationButtons = null; + } + NotificationButtonData clickedButton; + try { + clickedButton = NotificationButtonData.fromMap(data['clickedButton']); + } catch (e) { + clickedButton = null; + } + return NotificationData.create( + data['title'], + data['content'], + data['bigTitle'], + data['bigContent'], + data['summary'], + data['imageUrl'], + data['iconUrl'], + data['json'], + notificationButtons, + clickedButton); + } catch (e) { + return null; + } + } + + @override + String toString() => + 'NotificationData{_title: $_title, _content: $_content, _bigTitle: $_bigTitle, _bigContent: $_bigContent, _summary: $_summary, _imageUrl: $_imageUrl, _iconUrl: $_iconUrl, _customContent: $_customContent, buttons: $_buttons, clickedButton: $_clickedButton}'; + + get customContent => _customContent; + + get iconUrl => _iconUrl; + + get imageUrl => _imageUrl; + + get summary => _summary; + + get bigContent => _bigContent; + + get bigTitle => _bigTitle; + + get content => _content; + + get title => _title; + + get buttons => _buttons; + + get clickedButton => _clickedButton; +} + +/// +/// When there are buttons in the notification they are accessible through callbacks. +/// For every button there would be an object in the callback notification data object. +/// And also when a button is clicked, it's id and text will be passes separately in `onNotificationButtonClicked` callback. + +class NotificationButtonData { + String _title; + String _icon; + + NotificationButtonData.create(this._title, this._icon); + + String get title => _title; + + String get icon => _icon; + + @override + String toString() => 'NotificationButtonData{_title: $_title, _icon: $_icon}'; + + static NotificationButtonData fromMap(dynamic data) { + try { + return NotificationButtonData.create(data['title'], data['icon']); + } catch (e) { + return null; + } + } + + static List fromList(dynamic buttons) { + List list = buttons; + List buttonDataList = new List(); + for (var i in list) { + buttonDataList.add(NotificationButtonData.create(i['title'], i['icon'])); + } + return buttonDataList; + } +} + +// endregion + +// region InAppMessaging + +class InAppMessage { + String _title, _content; + List _buttons; + + get title => _title; + + get content => _content; + + get buttons => _buttons; + + InAppMessage.create(this._title, this._content, this._buttons); + + InAppMessage.fromDynamic(dynamic data) { + if (data == null) { + return; + } + _title = data['title'] as String; + _content = data['content'] as String; + List buttons = []; + try { + for (var i in data['buttons'] as List) { + buttons.add(InAppMessageButton.fromDynamic(i)); + } + } catch (e) { + print('Pushe: Failed to parse inApp buttons: $e}'); + } + _buttons = buttons; + } +} + +class InAppMessageButton { + String _text; + + get text => _text; + + InAppMessageButton(this._text); + + InAppMessageButton.fromDynamic(dynamic data) { + _text = data['text'] as String; + } +} + +// endregion diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..89ec533 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,147 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0-nullsafety.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0-nullsafety.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10-nullsafety.1" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0-nullsafety.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0-nullsafety.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0-nullsafety.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.19-nullsafety.2" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0-nullsafety.3" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0-nullsafety.3" +sdks: + dart: ">=2.10.0-110 <2.11.0" + flutter: ">=1.20.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9d58cf8 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,26 @@ +name: pushe_flutter +description: Pushe push notification SDK implementation for Flutter framework, for Android and iOS +version: 2.4.0 +homepage: https://pushe.co +documentation: https://docs.pushe.co/docs/flutter/intro/ +issue_tracker: https://github.com/pusheco/pushe-flutter/issues + +environment: + sdk: ">=2.7.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: co.pushe.plus.flutter + pluginClass: PusheFlutterPlugin + ios: + pluginClass: PusheFlutterPlugin \ No newline at end of file diff --git a/test/pushe_flutter_test.dart b/test/pushe_flutter_test.dart new file mode 100644 index 0000000..49e6968 --- /dev/null +++ b/test/pushe_flutter_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pushe_flutter/pushe.dart'; + +void main() { + const MethodChannel channel = MethodChannel('pushe_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + test('Passing', () {}); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); +}