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..e784e73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ + +*.iml + +.idea/ + +example/.metadata + +example/\.flutter-plugins-dependencies + +example/ios/Flutter/flutter_export_environment\.sh 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..5eb5ff4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,207 @@ +# ChangeLog + +## 2.5.2 +### Android +- [fix] Bridge methods casting issue + +## 2.5.1 +### Android +- [Breaking] **Huawei push support** android module is now optional and will not be added to classpath by default + In order to get benefits add it to your `build.gradle` file: + + ```groovy + //dependencies { + implementation("co.pushe.plus:hms:2.5.1") + // } + ``` + + +- [Breaking] `PusheInAppMessaging` android module is now optional and will not be added to classpath by default + In order to get benefits add it to your `build.gradle` file: + + ```groovy + //dependencies { + implementation("co.pushe.plus:inappmessaging:2.5.1") + // } + ``` + +- **Feat**: Update `targetSDK` to 31. This update also fixes the `android:exported` service attribute issue + +### iOS +- [**Breaking**]: iOS support is canceled temporarily until it is decided to bring it back. `Pushe.dart` method will ignore non-android calls and return default results + + +## 2.5.1-nullsafety.1 +- Fix: Remove legacy preview remote for downloading native packages + +## 2.5.1-nullsafety.0 +- Migration to null-safety + +## 2.5.0 +### Android +- Update native dependency to `2.5.0` + - **New**: Adds ability to ignore showing notification if app is open (either all notifications or individually using `show_foreground`) + - **New**: Location and Geofencing features are applied to `hms` module. Huawei devices can have location-related features + - Bug fixes and improvements +- **Feat:** New APIs for notification foreground awareness + - `enableNotificationForceForegroundAware`: Force enable foreground awareness for all notifications + - `disableNotificationForceForegroundAware`: Disable what was enabled by above function + - `isForceForegroundAware`: Is enabled or not + +## 2.4.2 +- Fix null-safe issue in `PusheChandler.kt` + +## 2.4.1 +### Android +- Update native module to `2.4.1` which includes: + - Bug fix for `hms` module + - Fix issues in registration +- Add `getFcmToken` and `getHmsToken` to return module tokens if needed +- Add `getActiveService` to return the currently chosen service to interact with (fcm,hms) +- Deprecate `getGoogleAdvertisingId`. Use `getAdvertisingId` instead. New method returns Huawei `OAID` when hms is used + +### iOS +- Unchanged + +## 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..414708c --- /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' + +buildscript { + ext.kotlin_version = '1.4.31' + repositories { + google() + mavenCentral() + maven {url 'https://developer.huawei.com/repo/'} + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + maven {url 'https://developer.huawei.com/repo/'} + } +} + + + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + def pushe_version = "2.5.1" + implementation ("co.pushe.plus:base:$pushe_version") + compileOnly ("co.pushe.plus:hms:$pushe_version") + compileOnly ("co.pushe.plus:inappmessaging:$pushe_version") + api "androidx.multidex:multidex:2.0.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..d9cf55d --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ 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..e878bca --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheChandler.kt @@ -0,0 +1,775 @@ +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.fcm.PusheFCM +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.notification.PusheNotification +import co.pushe.plus.notification.UserNotification +import io.flutter.FlutterInjector +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 + +/** + * 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.AdvertisingId", "IdType.CustomId") + + 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 fcmModule = Pushe.getPusheService(PusheFCM::class.java) + + 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.getAdvertisingId" -> result.success(Pushe.getAdvertisingId()) + "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) + "Pushe.disableInAppMessaging" -> disableInAppMessaging(result) + "Pushe.enableInAppMessaging" -> enableInAppMessaging(result) + "Pushe.isInAppMessagingEnabled" -> isInAppMessagingEnabled(result) + "Pushe.initializeInAppListeners" -> initializeInAppMessagingListeners() + "Pushe.dismissShownInApp" -> dismissShownInApp(result) + "Pushe.testInAppMessage" -> testInAppMessage(call, result) + "Pushe.getActiveCourier" -> getActiveService(result) + "Pushe.getFcmToken" -> getFcmTokenOrEmpty(result, fcmModule) + "Pushe.getHmsToken" -> getHmsTokenOrEmpty(result) + "Pushe.enableForceForegroundAware" -> enableForceForegroundAware(true, result, notificationModule) + "Pushe.disableForceForegroundAware" -> enableForceForegroundAware(false, result, notificationModule) + "Pushe.isForceForegroundAware" -> result.success(notificationModule?.isForegroundAwareByForce()) + 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.withDeviceId(id) + "IdType.AdvertisingId" -> 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.withDeviceId(id) + "IdType.AdvertisingId" -> 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) + } + + private fun enableForceForegroundAware(enabled: Boolean, result: MethodChannel.Result, notificationModule: PusheNotification?) { + if (notificationModule == null) { + result.error("030", "Notification module is not ready. Notification APIs will not ba handled.", null) + return + } + if (enabled) { + notificationModule.enableNotificationForceForegroundAware() + result.success(true) + } else { + notificationModule.disableNotificationForceForegroundAware() + result.success(true) + } + } + + // region InAppMessaging + private fun triggerEvent(call: MethodCall, result: MethodChannel.Result) { + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("023", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + 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) { + + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("024", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + + 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) { + + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("024", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + 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) { + + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("025", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + + if (piamModule == null) { + result.error("025", "InAppMessaging module is not available. InAppMessaging api will not operate.", null) + return + } + result.success(piamModule.isInAppMessagingEnabled()) + } + + private fun dismissShownInApp(result: MethodChannel.Result) { + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("026", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + + 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) { + + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + result.error("027", """ + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:${'$'}latest") in dependencies{} + """.trimIndent(), null) + return + } + val piamModule = Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + + 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) + } + + private fun initializeInAppMessagingListeners() { + try { + Pushe.getPusheService(co.pushe.plus.inappmessaging.PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + lg(""" + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:$\\latest + """.trimIndent()) + return + } + PusheInAppMessagingListener.setListeners(context) + } + + // endregion + + // region Fcm and Hms + + private fun getActiveService(result: MethodChannel.Result) { + Pushe.setInitializationCompleteListener { + result.success(Pushe.getActiveCourier() ?: "") + } + } + + private fun getFcmTokenOrEmpty(result: MethodChannel.Result, fcmModule: PusheFCM?) { + try { + result.success(fcmModule?.getToken() ?: "") + } catch (e: java.lang.Exception) { + result.error("028", "Failed to fetch token", null) + lg("Failed to fetch for Fcm Token") + } + } + + private fun getHmsTokenOrEmpty(result: MethodChannel.Result) { + try { + val hmsModule = Pushe.getPusheService(co.pushe.plus.hms.PusheHMS::class.java) + result.success(hmsModule?.getToken() ?: "") + } catch (e: NoClassDefFoundError) { + result.error("029", """ + PusheHMS module is not added into the classPath. + Since it's optional make sure you add + implementation("co.pushe.plus:hms:${'$'}latest") + in android/build.gradle in dependencies {} + """.trimIndent(), null) + } catch (e: java.lang.Exception) { + result.error("029", "Failed to fetch token", null) + lg("Failed to fetch for Hms Token") + } + } + + // 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 + FlutterInjector.instance().flutterLoader().ensureInitializationComplete(context.applicationContext, null) + val action = if (intent.action == null) "" else intent.action + if (action?.isEmpty() == true) { + 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..4e7ecb9 --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/PusheInAppMessagingListener.kt @@ -0,0 +1,81 @@ +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.flutter.Utils.lg +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) { + + try { + Pushe.getPusheService(PusheInAppMessaging::class.java) + } catch (e: NoClassDefFoundError) { + lg(""" + PusheInAppMessaging class does not exist in the classPath. + Make sure you add the library since its existence is optional + In you android/build.gradle add: + implementation("co.pushe.plus:inappmessaging:$\\latest + """.trimIndent()) + } + + 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..54f261a --- /dev/null +++ b/android/src/main/kotlin/co/pushe/plus/flutter/Utils.kt @@ -0,0 +1,231 @@ +package co.pushe.plus.flutter + +import android.content.Intent +import android.util.Log +import co.pushe.plus.flutter.Utils.lg +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: co.pushe.plus.inappmessaging.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..0feed90 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,65 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "co.pushe.sample.flutter" + minSdkVersion 17 + targetSdkVersion 30 + 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.13.2' + //noinspection GradleDependency + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" +} 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 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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..283b1e7 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,32 @@ +buildscript { + ext.kotlin_version = '1.4.31' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + mavenCentral() + } +} + +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..94adc3a --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +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..fba24f5 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jan 03 13:52:39 IRST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-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..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/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 0000000..3d43d11 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ 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 0000000..28c6bf0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ 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 0000000..f091b6b Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ 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 0000000..d0ef06e Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ 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 0000000..c8f9ed8 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ 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 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ 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 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ 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 0000000..75b2d16 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ 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 0000000..c4df70d Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ 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 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ 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 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ 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..f039720 --- /dev/null +++ b/example/lib/pushe_sample.dart @@ -0,0 +1,489 @@ +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.notificationClicked: + 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.5.2', + style: TextStyle(color: Colors.white)), + ), + preferredSize: Size.fromHeight(0)), + ), + 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.getAdvertisingId()}"); + }, + "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.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.AdvertisingId, + await Pushe.getAdvertisingId(), + '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.AdvertisingId, text, + 'Test title', 'Test content'); + _updateStatus('Sending notification to Advertising ID: $text'); + }); + }, + "Notification: CustomId": () async { + await getInfo( + context, + (text) { + Pushe.getCustomId().then((value) { + if (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); + } + ); + }, + "Fcm Token": () async { + var fcm = await Pushe.getFcmToken(); + var activeService = await Pushe.getActiveService(); + alert(context, () {}, title: 'Tokens', message: ''' +Fcm: $fcm +Active: $activeService + '''); + }, + "Hms Token": () async { + var hms = await Pushe.getHmsToken(); + var activeService = await Pushe.getActiveService(); + alert(context, () {}, title: 'Tokens', message: ''' +Hms: $hms +Active: $activeService + '''); + }, + "Notification: Toggle foreground awareness": () async { + if(await Pushe.isForceForegroundAware()) { + await Pushe.disableNotificationForceForegroundAware(); + } else { + await Pushe.enableNotificationForceForegroundAware(); + } + _updateStatus("Forced awareness to ${await Pushe.isForceForegroundAware()}"); + }, + "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..bada7d5 --- /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: [ + ElevatedButton( + child: Text(ok), + onPressed: () { + onOK(); + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + 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: [ + ElevatedButton( + child: Text(positive), + onPressed: () async { + Navigator.of(context).pop(); + await onPositive(result); + }, + ), + ElevatedButton( + 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..1cad8e0 --- /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.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + 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.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + pushe_flutter: + dependency: "direct dev" + description: + path: ".." + relative: true + source: path + version: "2.5.2" + 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.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" +sdks: + dart: ">=2.14.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..b1e2950 --- /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.5.2 + +environment: + sdk: '>=2.12.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: ^1.0.3 + +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..2e35731 --- /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/lib/pushe.dart b/lib/pushe.dart new file mode 100644 index 0000000..b8b6af7 --- /dev/null +++ b/lib/pushe.dart @@ -0,0 +1,846 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// +/// Enum: Notification id type +/// @author Mahdi Malvandi +/// +enum IdType { DeviceId, AdvertisingId, 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?.call(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 notificationClicked = '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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + return await _channel + .invokeMethod("Pushe.setUserConsentGiven", {"enabled": enabled}); + } + + /// Get the user consent status + static Future getUserConsentStatus() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.getUserConsentStatus"); + } + + /// + /// Get the unique id of the devices + /// Returns empty if native returned null + /// + static Future getDeviceId() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getDeviceId") ?? ""; + } + + @Deprecated('Use `getDeviceId` instead') + static Future getAndroidId() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getAndroidId") ?? ""; + } + + // android only + /// Get google advertising id + /// + /// **NOTE**: Since late 2021, if the user opted out of Ad personalization this id will be a bunch of zeros + @Deprecated("Use `getAdvertisingId` instead") + static Future getGoogleAdvertisingId() async { + if(!Platform.isAndroid) return ""; + + return await _channel.invokeMethod("Pushe.getGoogleAdvertisingId") ?? ""; + } + + /// + /// Return GoogleAdvertisingId (if using FCM) or AOID (if using HMS) + /// + /// **NOTE**: Since late 2021, if user opts out of Ad personalization, FCM ad_id will return bunch of zeros or empty + static Future getAdvertisingId() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getAdvertisingId") ?? ""; + } + + /// Get custom id + static Future getCustomId() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getCustomId") ?? ""; + } + + /// Set custom id + static Future setCustomId(String id) async { + if(!Platform.isAndroid) return; + return await _channel.invokeMethod("Pushe.setCustomId", {"id": id}); + } + + /// Get email + static Future getUserEmail() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getUserEmail") ?? ""; + } + + /// Set email + static Future setUserEmail(String email) async { + if(!Platform.isAndroid) return; + return await _channel.invokeMethod("Pushe.setUserEmail", {"email": email}); + } + + /// Get user phone number + static Future getUserPhoneNumber() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getUserPhoneNumber") ?? ""; + } + + /// Set user phone number + static Future setUserPhoneNumber(String phone) async { + if(!Platform.isAndroid) return; + return 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(!Platform.isAndroid) return; + 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(!Platform.isAndroid) return; + try { + if (await (_channel.invokeMethod("Pushe.removeTags", {"tags": tags}))) { + callback?.call(); + } + } catch(ignored) {} + return; + } + + /// Get subscribed tags + static Future getSubscribedTags() async { + if(!Platform.isAndroid) return {}; + return await _channel.invokeMethod("Pushe.getSubscribedTags") ?? {}; + } + + /// Get subscribed topics + static Future getSubscribedTopics() async { + if(!Platform.isAndroid) return []; + return 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(!Platform.isAndroid) return; + try { + if (await (_channel.invokeMethod("Pushe.subscribe", {"topic": topic}))) { + callback?.call(); + } + } catch (ignored) { + return; + } + 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(!Platform.isAndroid) return; + try { + if (await (_channel.invokeMethod("Pushe.unsubscribe", {"topic": topic}))) { + callback?.call(); + } + } catch(ignored) { + } + return; + } + + /// If this function is called, notification will not be shown. + static Future setNotificationOff() async { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.enableNotifications"); + } + + /// To check whether notification will publish or not (default is true of course) + static Future isNotificationOn() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isNotificationEnable") ?? true; + } + + /// 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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.enableCustomSound"); + } + + /// To check whether custom sound is already enabled or not + static Future isCustomSoundEnabled() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isCustomSoundEnabled") ?? true; + } + + /// Enables force to avoid displaying notification when the app is open + /// Instead only listeners will be called + static Future enableNotificationForceForegroundAware() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.enableForceForegroundAware") ?? false; + } + + /// Disables the behaviour set by `enableNotificationForceForegroundAware` + static Future disableNotificationForceForegroundAware() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.disableForceForegroundAware") ?? false; + } + + /// Returns true if foreground notification are published by force, false otherwise + static Future isForceForegroundAware() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isForceForegroundAware") ?? false; + } + + // region InAppMessage + + /// + /// Triggers a local event for inAppMessaging + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + /// + static Future triggerEvent(String eventName) async { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.triggerEvent", {'event': eventName}); + } + + /// + /// Disables InAppMessaging + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + static Future disableInAppMessaging() async { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.disableInAppMessaging"); + } + + /// + /// Enables InAppMessaging + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + static Future enableInAppMessaging() async { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.enableInAppMessaging"); + } + + /// + /// Checks whether InAppMessaging is enabled or not + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + static Future isInAppMessagingEnabled() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isInAppMessagingEnabled") ?? true; + } + + /// + /// Dismissed the shown InAppMessage + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + static Future dismissShownInApp() async { + if(!Platform.isAndroid) return; + await _channel.invokeMethod("Pushe.dismissShownInApp"); + } + + /// + /// Accepts a String which is the actual payload of an InAppMessage to show + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + @visibleForTesting + static Future testInAppMessage(String message, + {bool instant = false}) async { + if(!Platform.isAndroid) return; + await _channel.invokeMethod( + "Pushe.testInAppMessage", {'message': message, 'instant': instant}); + } + + /// + /// Sets a listener for InAppMessaging events + /// + /// **NOTE**: `PusheInAppMessaging` is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:inappmessaging:$latest")` in `dependencies {}` of `android/build.gradle` + static setInAppMessagingListener({ + Function(InAppMessage)? onReceived, + Function(InAppMessage)? onTriggered, + Function(InAppMessage)? onClicked, + Function(InAppMessage)? onDismissed, + Function(InAppMessage, int)? onButtonClicked, + }) async { + if(!Platform.isAndroid) return; + _inAppReceiveCallback = onReceived; + _inAppClickCallback = onClicked; + _inAppDismissCallback = onDismissed; + _inAppTriggerCallback = onTriggered; + _inAppButtonClickCallback = onButtonClicked; + await _channel.invokeMethod("Pushe.initializeInAppListeners"); + } + + // endregion + + // region FCM and HMS + + /// @return either `hms` or `fcm` or empty result if none of them were available. + /// **NOTE**: If anything was wrong in initialization, this may never return result + static Future getActiveService() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod('Pushe.getActiveCourier') ?? ""; + } + + /// @return Token of Firebase cloud messaging used by Pushe + /// and empty if anything was wrong or active service wasn't FCM + static Future getFcmToken() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getFcmToken") ?? ""; + } + + /// @return Token of Huawei PushKit used by Pushe + /// and empty if anything was wrong or active service wasn't HMS + /// + /// **NOTE**: HMS is an optional instance and you need to enable it by adding + /// `implementation("co.pushe.plus:hms:$latest")` in `dependencies {}` of `android/build.gradle` + static Future getHmsToken() async { + if(!Platform.isAndroid) return ""; + return await _channel.invokeMethod("Pushe.getHmsToken") ?? ""; + } + + // 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 { + if(!Platform.isAndroid) return; + 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) async { + if(!Platform.isAndroid) return; + return await _channel.invokeMethod( + "Pushe.removeNotificationChannel", {"channelId": channelId}); + } + + /// Check if Pushe is initialized to server or not. + static Future isInitialized() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isInitialized") ?? false; + } + + /// Check if Pushe is registered to server or not. + static Future isRegistered() async { + if(!Platform.isAndroid) return false; + return await _channel.invokeMethod("Pushe.isRegistered") ?? false; + } + + /// Call it's callback when registration is completed + static Future setRegistrationCompleteListener(Function callback) async { + if(!Platform.isAndroid) return; + dynamic result = + await _channel.invokeMethod("Pushe.setRegistrationCompleteListener") ?? false; + if (result) callback.call(); + return; + } + + /// Call it's callback when initialization is completed + static Future setInitializationCompleteListener( + Function callback) async { + if(!Platform.isAndroid) return; + dynamic 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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + 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 { + if(!Platform.isAndroid) return; + _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 { + if(!Platform.isAndroid) return; + 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.tryParse(arg['index']) ?? -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 = []; + 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..bf25015 --- /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.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + 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.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.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.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" +sdks: + dart: ">=2.14.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..ad869ca --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,24 @@ +name: pushe_flutter +description: Pushe push notification SDK implementation for Flutter framework, for Android and iOS +version: 2.5.2 +homepage: https://pushe.co +documentation: https://docs.pushe.co/docs/flutter/intro/ +issue_tracker: https://github.com/pusheco/pushe-flutter/issues + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter +flutter: + plugin: + platforms: + android: + package: co.pushe.plus.flutter + 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); + }); +}