├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── PushNotificationPlugin.h │ ├── PushNotificationPlugin.m │ └── SwiftPushNotificationPlugin.swift ├── .gitignore └── push_notification.podspec ├── .fvmrc ├── android ├── settings.gradle ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ └── strings.xml │ │ └── kotlin │ │ └── pushnotification │ │ └── push_notification │ │ ├── notification │ │ └── PushNotificationData.kt │ │ ├── type │ │ └── PushNotificationTypeData.kt │ │ ├── handler │ │ └── PushHandler.kt │ │ ├── strategy │ │ └── PushStrategy.kt │ │ └── PushNotificationPlugin.kt ├── values │ └── strings.xml ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── build.gradle ├── dart_dependency_validator.yaml ├── example ├── ios │ ├── Flutter │ │ ├── .last_build_id │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── Runner.entitlements │ │ ├── AppDelegate.swift │ │ ├── GoogleService-Info.plist │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── firebase_app_id_file.json │ ├── .gitignore │ └── Podfile ├── analysis_options.yaml ├── .fvm │ └── fvm_config.json ├── android │ ├── gradle.properties │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── google-services.json │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── build.gradle │ └── settings.gradle ├── scripts │ ├── package.json │ └── send-message.js ├── .metadata ├── pubspec.yaml ├── lib │ ├── utils │ │ └── logger.dart │ ├── notification │ │ ├── first_strategy.dart │ │ ├── second_strategy.dart │ │ ├── example_factory.dart │ │ └── messaging_service.dart │ ├── ui │ │ ├── app.dart │ │ ├── first_screen.dart │ │ ├── second_screen.dart │ │ ├── message_list.dart │ │ └── main_screen.dart │ ├── domain │ │ └── message.dart │ └── main.dart └── .gitignore ├── analysis_options.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation_update.md │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE │ ├── documentation_update.md │ ├── new_feature.md │ └── bug_fix.md └── workflows │ ├── on_close_issue_add_comment_to_pyrus_task.yml │ ├── on_close_pr_add_comment_to_pyrus_task.yml │ ├── on_pull_request.yml │ ├── publish_to_pub.yml │ ├── on_open_issue_create_pyrus_task.yml │ └── on_open_pr_create_pyrus_task.yml ├── .metadata ├── lib ├── src │ ├── notification │ │ ├── notificator │ │ │ ├── ios │ │ │ │ ├── ios_notification_specifics.dart │ │ │ │ └── ios_notification.dart │ │ │ ├── notification_specifics.dart │ │ │ ├── android │ │ │ │ ├── android_notiffication_specifics.dart │ │ │ │ └── android_notification.dart │ │ │ └── notificator.dart │ │ └── notification_controller.dart │ ├── util │ │ └── platform_wrapper.dart │ ├── push_navigator_holder.dart │ ├── base │ │ ├── notification_payload.dart │ │ ├── base_messaging_service.dart │ │ ├── push_handle_strategy_factory.dart │ │ └── push_handle_strategy.dart │ ├── push_observer.dart │ └── push_handler.dart └── push_notification.dart ├── pubspec.yaml ├── test ├── platform_wrapper_test.dart ├── push_navigator_holder_test.dart ├── push_observer_test.dart ├── push_handle_strategy_test.dart ├── android_notification_test.dart ├── notification_controller_test.dart ├── ios_notifiation_test.dart ├── push_handler_test.dart ├── push_handle_strategy_factory_test.dart └── notificator_test.dart ├── CONTRIBUTING.md ├── CHANGELOG.md ├── .gitignore ├── README.md └── LICENSE /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.19.5" 3 | } -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'push_notification' 2 | -------------------------------------------------------------------------------- /dart_dependency_validator.yaml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - "example/**" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 25930036dcbf7351ddfc501cfd18d63f -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:surf_lint_rules/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:surf_lint_rules/analysis_options.yaml 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.19.5", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /ios/Classes/PushNotificationPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PushNotificationPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .fvm/flutter_sdk 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: SurfGear telegram chat 4 | url: https://t.me/SurfGear 5 | about: Please ask and answer questions here. -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notification-scripts", 3 | "description": "Used to demonstrate sending a RemoteMessage to a client.", 4 | "dependencies": { 5 | "firebase-admin": "^11.5.0" 6 | } 7 | } -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/flutter-push-notification/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CHANNEL_ID 4 | Data push channel 5 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CHANNEL_ID 4 | Data push channel 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Tap on "Preview" ⤴ 2 | 3 | And choose one of the templates: 4 | 5 | * [Bugfix PR](?expand=1&template=bug_fix.md) 6 | * [New feature PR](?expand=1&template=new_feature.md) 7 | * [Documentation update PR](?expand=1&template=documentation_update.md) 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:1090377973523:ios:9575e0dd7f3de766d724e6", 5 | "FIREBASE_PROJECT_ID": "surf-push-notification-demo", 6 | "GCM_SENDER_ID": "1090377973523" 7 | } -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | allprojects { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | rootProject.buildDir = '../build' 10 | subprojects { 11 | project.buildDir = "${rootProject.buildDir}/${project.name}" 12 | } 13 | subprojects { 14 | project.evaluationDependsOn(':app') 15 | } 16 | 17 | tasks.register("clean", Delete) { 18 | delete rootProject.buildDir 19 | } 20 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: push_demo 2 | description: A new Flutter application. 3 | version: 1.0.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=3.2.0 <4.0.0" 8 | 9 | dependencies: 10 | firebase_core: 2.28.0 11 | firebase_messaging: 14.8.0 12 | flutter: 13 | sdk: flutter 14 | logger: 1.0.0 15 | push_notification: 16 | path: ../ 17 | 18 | dev_dependencies: 19 | surf_lint_rules: 3.0.0 20 | 21 | flutter: 22 | uses-material-design: true 23 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | import FirebaseCore 4 | 5 | @UIApplicationMain 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | FirebaseApp.configure() 12 | GeneratedPluginRegistrant.register(with: self) 13 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/src/main/kotlin/pushnotification/push_notification/notification/PushNotificationData.kt: -------------------------------------------------------------------------------- 1 | package pushnotification.push_notification.notification 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Notification model 7 | * 8 | * @param notificationData custom data in notification 9 | */ 10 | class PushNotificationData( 11 | val notificationData: Map 12 | ) : Serializable { 13 | override fun toString(): String { 14 | return "PushNotificationData(notificationData=$notificationData)" 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Classes/PushNotificationPlugin.m: -------------------------------------------------------------------------------- 1 | #import "PushNotificationPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "push_notification-Swift.h" 9 | #endif 10 | 11 | @implementation PushNotificationPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftPushNotificationPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/documentation_update.md: -------------------------------------------------------------------------------- 1 | 6 | ## Checklist 7 | 8 | - [ ] Is there an existing issue for this PR? 9 | - _link issue here_ (use keywords like `fix`, `close`, `resolve` etc. if necessary) 10 | - [ ] Have the files been linted and formatted? 11 | 12 | ## Changes 13 | 14 | ### What docs page needs to be fixed? 15 | 16 | - **Section**: 17 | - **Page**: 18 | 19 | ## What is the problem? 20 | 21 | ## What changes does this PR make to fix the problem? 22 | -------------------------------------------------------------------------------- /example/lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:logger/logger.dart'; 16 | 17 | final logger = Logger(); 18 | -------------------------------------------------------------------------------- /android/src/main/kotlin/pushnotification/push_notification/type/PushNotificationTypeData.kt: -------------------------------------------------------------------------------- 1 | package pushnotification.push_notification.type 2 | 3 | import pushnotification.push_notification.notification.PushNotificationData 4 | import ru.surfstudio.android.notification.interactor.push.BaseNotificationTypeData 5 | 6 | /** 7 | * Push type with data 8 | */ 9 | class PushNotificationTypeData : BaseNotificationTypeData() { 10 | 11 | /**todo Данные из флаттера могут приходить в любом формате, не только в строках 12 | * необходимо в [BaseNotificationTypeData] в методе setDataFromMap поменять параметр String 13 | * на dymamic**/ 14 | override fun extractData(map: Map): PushNotificationData = PushNotificationData(map) 15 | } -------------------------------------------------------------------------------- /lib/src/notification/notificator/ios/ios_notification_specifics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Ios notification settings. 16 | class IosNotificationSpecifics {} 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_update.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation update 3 | about: Fixing a problem or improve in an existing docs page. 4 | labels: documentation 5 | --- 6 | 10 | ## Describe what scenario you think is uncovered by the existing examples / articles 11 | 12 | 13 | ## Describe why existing examples / articles do not cover this case 14 | 15 | 16 | ## Additional context 17 | 18 | -------------------------------------------------------------------------------- /example/android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "1090377973523", 4 | "project_id": "surf-push-notification-demo", 5 | "storage_bucket": "surf-push-notification-demo.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:1090377973523:android:f32ad32128db924cd724e6", 11 | "android_client_info": { 12 | "package_name": "com.example.push_demo" 13 | } 14 | }, 15 | "oauth_client": [], 16 | "api_key": [ 17 | { 18 | "current_key": "AIzaSyDDUXva7CkcATCt_gNwJS_2KeCAK3H9VAs" 19 | } 20 | ], 21 | "services": { 22 | "appinvite_service": { 23 | "other_platform_oauth_client": [] 24 | } 25 | } 26 | } 27 | ], 28 | "configuration_version": "1" 29 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: push_notification 2 | version: 2.0.2 3 | description: Library for implementing push notifications. The module contains the main work with push notifications. 4 | repository: "https://github.com/surfstudio/flutter-push-notification" 5 | issue_tracker: "https://github.com/surfstudio/flutter-push-notification/issues" 6 | 7 | dependencies: 8 | flutter: 9 | sdk: flutter 10 | rxdart: ^0.27.0 11 | 12 | dev_dependencies: 13 | flutter_test: 14 | sdk: flutter 15 | mocktail: ^0.1.2 16 | surf_lint_rules: ^3.0.0 17 | 18 | environment: 19 | sdk: ">=3.0.0 <4.0.0" 20 | flutter: ">=3.0.0" 21 | 22 | flutter: 23 | plugin: 24 | platforms: 25 | android: 26 | package: pushnotification.push_notification 27 | pluginClass: PushNotificationPlugin 28 | ios: 29 | pluginClass: PushNotificationPlugin 30 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.1.2" apply false 22 | id "org.jetbrains.kotlin.android" version "1.6.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /lib/src/util/platform_wrapper.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io'; 16 | 17 | /// Wrapper for Platform io. 18 | class PlatformWrapper { 19 | bool get isAndroid => Platform.isAndroid; 20 | 21 | bool get isIOS => Platform.isIOS; 22 | } 23 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/on_close_issue_add_comment_to_pyrus_task.yml: -------------------------------------------------------------------------------- 1 | name: Add comment to Pyrus task on issue close 2 | 3 | on: 4 | issues: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | get_pyrus_task_id: 10 | name: Get Pyrus task ID from special comment in issue 11 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_pyrus_task_id_from_issue_or_pr_comment.yml@main 12 | with: 13 | ISSUE_NUMBER: ${{ github.event.issue.number }} 14 | 15 | add_comment_to_pyrus_task: 16 | name: Add comment to Pyrus task 17 | needs: get_pyrus_task_id 18 | uses: surfstudio/flutter-ci-workflows/.github/workflows/add_comment_to_pyrus_task.yml@main 19 | with: 20 | PYRUS_TASK_ID: ${{ needs.get_pyrus_task_id.outputs.PYRUS_TASK_ID }} 21 | COMMENT_TEXT: The issue is closed. 22 | secrets: 23 | LOGIN: ${{ secrets.PYRUS_BOT_LOGIN }} 24 | SECURITY_KEY: ${{ secrets.PYRUS_BOT_SECRET_KEY }} 25 | -------------------------------------------------------------------------------- /.github/workflows/on_close_pr_add_comment_to_pyrus_task.yml: -------------------------------------------------------------------------------- 1 | name: Add comment to Pyrus task on PR close 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | get_pyrus_task_id: 10 | name: Get Pyrus task ID from special comment in issue 11 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_pyrus_task_id_from_issue_or_pr_comment.yml@main 12 | with: 13 | ISSUE_NUMBER: ${{ github.event.pull_request.number }} 14 | 15 | add_comment_to_pyrus_task: 16 | name: Add comment to Pyrus task 17 | needs: get_pyrus_task_id 18 | uses: surfstudio/flutter-ci-workflows/.github/workflows/add_comment_to_pyrus_task.yml@main 19 | with: 20 | PYRUS_TASK_ID: ${{ needs.get_pyrus_task_id.outputs.PYRUS_TASK_ID }} 21 | COMMENT_TEXT: The pull request is closed. 22 | secrets: 23 | LOGIN: ${{ secrets.PYRUS_BOT_LOGIN }} 24 | SECURITY_KEY: ${{ secrets.PYRUS_BOT_SECRET_KEY }} 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | 6 | ## Checklist 7 | 8 | - [ ] Have you added an explanation of what your changes do and why you'd like us to include them? 9 | - [ ] Is there an existing issue for this PR? 10 | - _link issue here_ (use keywords like `fix`, `close`, `resolve` etc. if necessary) 11 | - [ ] Have the files been linted and formatted? 12 | - [ ] Have the docs been updated to match the changes in the PR? 13 | - [ ] Have the tests been updated to match the changes in the PR? 14 | - [ ] Attached videos/screenshots demonstrating the fix/feature. 15 | - [ ] Have you run the tests locally to confirm they pass? 16 | 17 | ## New Features 18 | 19 | ### What new capabilities does this PR add? 20 | 21 | ### What docs changes are needed to explain this? 22 | -------------------------------------------------------------------------------- /ios/push_notification.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint push_notification.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'push_notification' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter plugin.' 9 | s.description = <<-DESC 10 | A new Flutter plugin. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '10.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug_fix.md: -------------------------------------------------------------------------------- 1 | 6 | ## Checklist 7 | 8 | - [ ] Have you added an explanation of what your changes do and why you'd like us to include them? 9 | - [ ] Is there an existing issue for this PR? 10 | - _link issue here_ (use keywords like `fix`, `close`, `resolve` etc. if necessary) 11 | - [ ] Have the files been linted and formatted? 12 | - [ ] Have the docs been updated to match the changes in the PR? 13 | - [ ] Have the tests been updated to match the changes in the PR? 14 | - [ ] Attached videos/screenshots demonstrating the fix/feature. 15 | - [ ] Have you run the tests locally to confirm they pass? 16 | 17 | ## Changes 18 | 19 | ### What is the current behavior, and the steps to reproduce the issue? 20 | 21 | ### What is the expected behavior? 22 | 23 | ### How does this PR fix the problem? 24 | -------------------------------------------------------------------------------- /example/ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_KEY 6 | AIzaSyAdRZCf8FyNj0v_jJZSh-ovHdW8AQlocgw 7 | GCM_SENDER_ID 8 | 1090377973523 9 | PLIST_VERSION 10 | 1 11 | BUNDLE_ID 12 | com.example.demo-push-notification 13 | PROJECT_ID 14 | surf-push-notification-demo 15 | STORAGE_BUCKET 16 | surf-push-notification-demo.appspot.com 17 | IS_ADS_ENABLED 18 | 19 | IS_ANALYTICS_ENABLED 20 | 21 | IS_APPINVITE_ENABLED 22 | 23 | IS_GCM_ENABLED 24 | 25 | IS_SIGNIN_ENABLED 26 | 27 | GOOGLE_APP_ID 28 | 1:1090377973523:ios:9575e0dd7f3de766d724e6 29 | 30 | -------------------------------------------------------------------------------- /lib/src/push_navigator_holder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/widgets.dart'; 16 | 17 | /// Global navigator context storage. 18 | class PushNavigatorHolder { 19 | static final _instance = PushNavigatorHolder._internal(); 20 | 21 | NavigatorState? navigator; 22 | 23 | static PushNavigatorHolder get instance => _instance; 24 | 25 | factory PushNavigatorHolder() => _instance; 26 | 27 | PushNavigatorHolder._internal(); 28 | } 29 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: I'd like SurfGear team to do something new. 4 | labels: enhancement 5 | --- 6 | 12 | ## What is the new or updated feature that you are suggesting? 13 | 14 | 15 | ## Why should this feature be included? 16 | 17 | 18 | ## Additional context 19 | -------------------------------------------------------------------------------- /lib/src/base/notification_payload.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Notification base payload data. 16 | abstract class NotificationPayload { 17 | /// Original message. 18 | final Map messageData; 19 | 20 | /// Fields required to show notification. 21 | final String title; 22 | final String body; 23 | 24 | final String? imageUrl; 25 | 26 | const NotificationPayload( 27 | this.messageData, 28 | this.title, 29 | this.body, { 30 | this.imageUrl, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/kotlin/pushnotification/push_notification/handler/PushHandler.kt: -------------------------------------------------------------------------------- 1 | package pushnotification.push_notification.handler 2 | 3 | import android.content.Context 4 | import ru.surfstudio.android.activity.holder.ActiveActivityHolder 5 | import ru.surfstudio.android.notification.interactor.push.PushInteractor 6 | import pushnotification.push_notification.strategy.PushStrategy 7 | 8 | /** Push notification handler **/ 9 | class PushHandler( 10 | private val activeActivityHolder: ActiveActivityHolder, 11 | private val pushInteractor: PushInteractor 12 | ) { 13 | 14 | fun handleMessage(context: Context, 15 | uniqueId: Int, 16 | title: String, 17 | body: String, 18 | pushHandleStrategy: PushStrategy) { 19 | val activity = activeActivityHolder.activity 20 | pushHandleStrategy.handle( 21 | context = activity ?: context, 22 | pushInteractor = pushInteractor, 23 | uniqueId = uniqueId, 24 | title = title, 25 | body = body 26 | ) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lib/src/push_observer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/widgets.dart'; 16 | import 'package:push_notification/src/push_navigator_holder.dart'; 17 | 18 | /// Mixin to get navigator context. 19 | class PushObserver extends NavigatorObserver { 20 | @override 21 | void didPush(Route route, Route? previousRoute) { 22 | PushNavigatorHolder().navigator = navigator; 23 | } 24 | 25 | @override 26 | void didPop(Route route, Route? previousRoute) { 27 | PushNavigatorHolder().navigator = navigator; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'pushnotification.push_notification' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.6.10' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.1.3' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | apply plugin: 'kotlin-android-extensions' 27 | 28 | android { 29 | compileSdk 33 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | defaultConfig { 35 | minSdkVersion 19 36 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 37 | } 38 | lintOptions { 39 | disable 'InvalidPackage' 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 45 | implementation "ru.surfstudio.android:push:0.5.0" 46 | implementation "ru.surfstudio.android:activity-holder:0.5.0" 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/notification/notificator/notification_specifics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:push_notification/src/notification/notificator/android/android_notiffication_specifics.dart'; 16 | import 'package:push_notification/src/notification/notificator/ios/ios_notification_specifics.dart'; 17 | 18 | /// Specific notification settings for platforms. 19 | class NotificationSpecifics { 20 | /// Settings for Android. 21 | AndroidNotificationSpecifics androidNotificationSpecifics; 22 | 23 | /// Settings for iOS. 24 | IosNotificationSpecifics? iosNotificationSpecifics; 25 | 26 | NotificationSpecifics(this.androidNotificationSpecifics); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: On pull request 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | get_flutter_version: 8 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_flutter_version.yml@main 9 | 10 | analysis: 11 | needs: get_flutter_version 12 | uses: surfstudio/flutter-ci-workflows/.github/workflows/analysis.yml@main 13 | with: 14 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 15 | 16 | testing: 17 | needs: [analysis, get_flutter_version] 18 | uses: surfstudio/flutter-ci-workflows/.github/workflows/testing.yml@main 19 | with: 20 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 21 | secrets: 22 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 23 | 24 | build_android_example: 25 | needs: [analysis, get_flutter_version] 26 | uses: surfstudio/flutter-ci-workflows/.github/workflows/build_android_example.yml@main 27 | with: 28 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 29 | 30 | build_ios_example: 31 | needs: [analysis, get_flutter_version] 32 | uses: surfstudio/flutter-ci-workflows/.github/workflows/build_ios_example.yml@main 33 | with: 34 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 35 | -------------------------------------------------------------------------------- /.github/workflows/publish_to_pub.yml: -------------------------------------------------------------------------------- 1 | name: Publishing 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | get_flutter_version: 10 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_flutter_version.yml@main 11 | 12 | analysis: 13 | needs: get_flutter_version 14 | uses: surfstudio/flutter-ci-workflows/.github/workflows/analysis.yml@main 15 | with: 16 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 17 | 18 | testing: 19 | needs: [analysis, get_flutter_version] 20 | uses: surfstudio/flutter-ci-workflows/.github/workflows/testing.yml@main 21 | with: 22 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 23 | secrets: 24 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 25 | 26 | package-publishing: 27 | needs: [analysis, testing, get_flutter_version] 28 | uses: surfstudio/flutter-ci-workflows/.github/workflows/publish_to_pub.yml@main 29 | with: 30 | FLUTTER_VERSION: ${{ needs.get_flutter_version.outputs.FLUTTER_VERSION }} 31 | PANA_TOTAL: "100" 32 | secrets: 33 | PUB_CREDENTIAL_JSON: ${{ secrets.SURF_PUB_CREDENTIAL_JSON }} 34 | PUB_OAUTH_ACCESS_TOKEN: ${{ secrets.SURF_PUB_OAUTH_ACCESS_TOKEN }} 35 | PUB_OAUTH_REFRESH_TOKEN: ${{ secrets.SURF_PUB_OAUTH_REFRESH_TOKEN }} 36 | -------------------------------------------------------------------------------- /test/platform_wrapper_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License.. 14 | 15 | import 'package:flutter_test/flutter_test.dart'; 16 | import 'package:push_notification/src/util/platform_wrapper.dart'; 17 | 18 | void main() { 19 | late PlatformWrapper platformWrapper; 20 | 21 | setUp( 22 | () { 23 | platformWrapper = PlatformWrapper(); 24 | }, 25 | ); 26 | 27 | test( 28 | 'Method isAndroid should return false', 29 | () { 30 | final isAndroid = platformWrapper.isAndroid; 31 | 32 | expect(isAndroid, false); 33 | }, 34 | ); 35 | 36 | test( 37 | 'Method isIOS should return false', 38 | () { 39 | final isIOS = platformWrapper.isIOS; 40 | 41 | expect(isIOS, false); 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/on_open_issue_create_pyrus_task.yml: -------------------------------------------------------------------------------- 1 | name: Create Pyrus task from new issue 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | 9 | jobs: 10 | get_package_name: 11 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_package_name.yml@main 12 | 13 | create_pyrus_task: 14 | name: Create Pyrus task from issue 15 | needs: get_package_name 16 | uses: surfstudio/flutter-ci-workflows/.github/workflows/create_pyrus_task_from_issue_or_pr.yml@main 17 | with: 18 | TYPE: Issue 19 | PACKAGE_NAME: ${{ needs.get_package_name.outputs.package_name }} 20 | TITLE: ${{ github.event.issue.title }} 21 | LINK: ${{ github.event.issue.html_url }} 22 | secrets: 23 | LOGIN: ${{ secrets.PYRUS_BOT_LOGIN }} 24 | SECURITY_KEY: ${{ secrets.PYRUS_BOT_SECRET_KEY }} 25 | 26 | add_pyrus_task_link: 27 | name: Add Pyrus task link 28 | needs: create_pyrus_task 29 | uses: surfstudio/flutter-ci-workflows/.github/workflows/add_comment_to_issue_or_pr.yml@main 30 | with: 31 | ISSUE_NUMBER: ${{ github.event.issue.number }} 32 | # Don't change the comment link, because the Pyrus task ID is searched by the link text 33 | COMMENT_TEXT: "[Link to Pyrus task](https://pyrus.com/t#id${{ needs.create_pyrus_task.outputs.PYRUS_TASK_ID }})" 34 | REACTIONS: rocket 35 | -------------------------------------------------------------------------------- /example/scripts/send-message.js: -------------------------------------------------------------------------------- 1 | var admin = require('firebase-admin'); 2 | var serviceAccount = require('./google-services.json'); 3 | admin.initializeApp({ 4 | credential: admin.credential.cert(serviceAccount), 5 | }); 6 | 7 | const token = ''; 8 | 9 | const eventType = process.argv[2]; 10 | 11 | admin.messaging().send( 12 | { 13 | token: token, 14 | notification: { 15 | title: 'Hello', 16 | body: 'This is notification of ' + eventType, 17 | }, 18 | data: { 19 | extraInt: '1', 20 | extraDouble: '1.0', 21 | event: eventType, 22 | }, 23 | android: { 24 | // Required for background/terminated app state messages on Android 25 | priority: 'high', 26 | }, 27 | apns: { 28 | payload: { 29 | aps: { 30 | // Required for background/terminated app state messages on iOS 31 | contentAvailable: true, 32 | }, 33 | }, 34 | }, 35 | }, 36 | ) 37 | .then((res) => { 38 | if (res.failureCount) { 39 | console.log('Failed', res.results[0].error); 40 | } else { 41 | console.log('Success'); 42 | } 43 | }) 44 | .catch((err) => { 45 | console.log('Error:', err); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/src/base/base_messaging_service.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:push_notification/src/push_handler.dart'; 16 | 17 | /// Base wrapper over any message service. 18 | // ignore: one_member_abstracts 19 | abstract class BaseMessagingService { 20 | /// No need to call. Initialization is called inside the [PushHandler]. 21 | void initNotification(HandleMessageFunction handleMessage); 22 | } 23 | 24 | enum MessageHandlerType { 25 | /// The message that is received when the app is in the foreground. 26 | onMessage, 27 | 28 | /// The message that is received when the app is in the background or terminated. 29 | onBackgroundMessage, 30 | 31 | /// The message that initiated the opening of the application. 32 | onMessageOpenedApp, 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/on_open_pr_create_pyrus_task.yml: -------------------------------------------------------------------------------- 1 | name: Create Pyrus task from new PR 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | 9 | jobs: 10 | get_package_name: 11 | uses: surfstudio/flutter-ci-workflows/.github/workflows/get_package_name.yml@main 12 | 13 | create_pyrus_task: 14 | name: Create Pyrus task from pull request 15 | needs: get_package_name 16 | uses: surfstudio/flutter-ci-workflows/.github/workflows/create_pyrus_task_from_issue_or_pr.yml@main 17 | with: 18 | TYPE: Pull request 19 | PACKAGE_NAME: ${{ needs.get_package_name.outputs.package_name }} 20 | TITLE: ${{ github.event.pull_request.title }} 21 | LINK: ${{ github.event.pull_request.html_url }} 22 | secrets: 23 | LOGIN: ${{ secrets.PYRUS_BOT_LOGIN }} 24 | SECURITY_KEY: ${{ secrets.PYRUS_BOT_SECRET_KEY }} 25 | 26 | add_pyrus_task_link: 27 | name: Add Pyrus task link 28 | needs: create_pyrus_task 29 | uses: surfstudio/flutter-ci-workflows/.github/workflows/add_comment_to_issue_or_pr.yml@main 30 | with: 31 | ISSUE_NUMBER: ${{ github.event.pull_request.number }} 32 | # Don't change the comment link, because the Pyrus task ID is searched by the link text 33 | COMMENT_TEXT: "[Link to Pyrus task](https://pyrus.com/t#id${{ needs.create_pyrus_task.outputs.PYRUS_TASK_ID }})" 34 | REACTIONS: rocket 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing rules 2 | 3 | Thank you for your help! Before you start, let's take a look at some agreements. 4 | 5 | ## Pull request rules 6 | 7 | Make sure that your code: 8 | 9 | 1. Does not contain analyzer errors. 10 | 2. Follows a [official style](https://dart.dev/guides/language/effective-dart/style). 11 | 3. Follows the official [style of formatting](https://flutter.dev/docs/development/tools/formatting). 12 | 4. Contains no errors. 13 | 5. New functionality is covered by tests. New functionality passes old tests. 14 | 6. Create example that demonstrate new functionality if it is possible. 15 | 16 | ## Accepting the changes 17 | 18 | After your pull request passes the review code, the project maintainers will merge the changes 19 | into the branch to which the pull request was sent. 20 | 21 | ## Issues 22 | 23 | Feel free to report any issues and bugs. 24 | 25 | 1. To report about the problem, create an issue on GitHub. 26 | 2. In the issue add the description of the problem. 27 | 3. Do not forget to mention your development environment, Flutter version, libraries required for 28 | illustration of the problem. 29 | 4. It is necessary to attach the code part that causes an issue or to make a small demo project 30 | that shows the issue. 31 | 5. Attach stack trace so it helps us to deal with the issue. 32 | 6. If the issue is related to graphics, screen recording is required. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is wrong. 4 | title: "[BUG] " 5 | labels: bug 6 | --- 7 | 11 | ## Expected behavior 12 | 13 | 14 | ## Actual behavior 15 | 16 | 17 | ## Video/Screenshot 18 | 19 | 20 | ## Steps to reproduce 21 | 27 | 28 | ## Details 29 | 30 | Flutter version: 31 | 32 | Dart version: 33 | 34 | Platform: 35 | 36 | ## Logs and stacktrace 37 | 40 | 41 | ## Any possible solutions 42 | 46 | 47 | ## What did you try to solve 48 | 52 | 53 | ## Checklist for self-check 54 | 55 | - [ ] Added expected and actual behavior. 56 | - [ ] Added video or screenshot of bug. 57 | - [ ] Added isolated way to reproduce the bug. 58 | - [ ] Specified Flutter, Dart version and platforms. 59 | - [ ] Attached error code and logs. 60 | - [ ] All unspecified fields in the Issue description are deleted. 61 | -------------------------------------------------------------------------------- /example/lib/notification/first_strategy.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | import 'package:push_demo/ui/first_screen.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | class FirstStrategy extends PushHandleStrategy { 21 | FirstStrategy(super.payload); 22 | 23 | @override 24 | void onTapNotification(NavigatorState? navigator) { 25 | debugPrint('on tap notification'); 26 | 27 | navigator?.push( 28 | MaterialPageRoute( 29 | builder: (context) => FirstScreen(payload), 30 | ), 31 | ); 32 | } 33 | 34 | @override 35 | void onBackgroundProcess(Map message) { 36 | debugPrint('on process notification in background'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/push_navigator_holder_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter_test/flutter_test.dart'; 16 | import 'package:push_notification/push_notification.dart'; 17 | 18 | void main() { 19 | test( 20 | 'PushNavigatorHolder.instance method call should always return the same ' 21 | 'PushNavigatorHolder instance', 22 | () { 23 | final firstInstancePushNavigatorHolder = PushNavigatorHolder.instance; 24 | final secondInstancePushNavigatorHolder = PushNavigatorHolder.instance; 25 | 26 | expect(firstInstancePushNavigatorHolder, PushNavigatorHolder()); 27 | expect(secondInstancePushNavigatorHolder, PushNavigatorHolder()); 28 | expect( 29 | firstInstancePushNavigatorHolder, 30 | same(secondInstancePushNavigatorHolder), 31 | ); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /example/lib/notification/second_strategy.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | import 'package:push_demo/ui/second_screen.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | class SecondStrategy extends PushHandleStrategy { 21 | SecondStrategy(super.payload); 22 | 23 | @override 24 | void onTapNotification(NavigatorState? navigator) { 25 | debugPrint('on tap notification'); 26 | 27 | navigator?.push( 28 | MaterialPageRoute( 29 | builder: (context) => SecondScreen(payload), 30 | ), 31 | ); 32 | } 33 | 34 | @override 35 | void onBackgroundProcess(Map message) { 36 | debugPrint('on process notification in background'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/lib/ui/app.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/notification/messaging_service.dart'; 17 | import 'package:push_demo/ui/main_screen.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | class MyApp extends StatelessWidget { 21 | final PushHandler _pushHandler; 22 | final MessagingService _messagingService; 23 | 24 | const MyApp(this._pushHandler, this._messagingService, {super.key}); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return MaterialApp( 29 | navigatorObservers: [PushObserver()], 30 | title: 'Push demo', 31 | theme: ThemeData(primarySwatch: Colors.indigo), 32 | home: MainScreen( 33 | pushHandler: _pushHandler, messagingService: _messagingService), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/lib/ui/first_screen.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | 18 | const String androidMipMapIcon = '@mipmap/ic_launcher'; 19 | 20 | class FirstScreen extends StatelessWidget { 21 | final Message payload; 22 | 23 | const FirstScreen(this.payload, {super.key}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: const Text('First notification screen'), 30 | ), 31 | body: Center( 32 | child: Column( 33 | children: [ 34 | const Text('Incoming message'), 35 | Text('Title : ${payload.title}'), 36 | Text('Body: ${payload.body}'), 37 | ], 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/lib/domain/message.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:push_notification/push_notification.dart'; 16 | 17 | class Message extends NotificationPayload { 18 | final int extraInt; 19 | final double extraDouble; 20 | 21 | const Message( 22 | super.messageData, 23 | super.title, 24 | super.body, 25 | this.extraInt, 26 | this.extraDouble, 27 | ); 28 | 29 | factory Message.fromMap(Map map) { 30 | final data = map['data'] as Map; 31 | final notification = map['notification'] as Map; 32 | 33 | return Message( 34 | map, 35 | notification['title'] as String, 36 | notification['body'] as String, 37 | int.tryParse(data['extraInt'].toString()) ?? 0, 38 | double.tryParse(data['extraDouble'].toString()) ?? 0, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/lib/ui/second_screen.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | 18 | const String androidMipMapIcon = '@mipmap/ic_launcher'; 19 | 20 | class SecondScreen extends StatelessWidget { 21 | final Message payload; 22 | 23 | const SecondScreen(this.payload, {super.key}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: const Text('Second notification screen'), 30 | ), 31 | body: Center( 32 | child: Column( 33 | children: [ 34 | const Text('Incoming message'), 35 | Text('Title : ${payload.title}'), 36 | Text('Body: ${payload.body}'), 37 | ], 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/push_notification.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export 'package:push_notification/src/base/base_messaging_service.dart'; 16 | export 'package:push_notification/src/base/notification_payload.dart'; 17 | export 'package:push_notification/src/base/push_handle_strategy.dart'; 18 | export 'package:push_notification/src/base/push_handle_strategy_factory.dart'; 19 | export 'package:push_notification/src/notification/notification_controller.dart'; 20 | export 'package:push_notification/src/notification/notificator/android/android_notiffication_specifics.dart'; 21 | export 'package:push_notification/src/notification/notificator/notification_specifics.dart'; 22 | export 'package:push_notification/src/notification/notificator/notificator.dart'; 23 | export 'package:push_notification/src/push_handler.dart'; 24 | export 'package:push_notification/src/push_navigator_holder.dart'; 25 | export 'package:push_notification/src/push_observer.dart'; 26 | -------------------------------------------------------------------------------- /example/lib/notification/example_factory.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:push_demo/domain/message.dart'; 16 | import 'package:push_demo/notification/first_strategy.dart'; 17 | import 'package:push_demo/notification/second_strategy.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | class ExampleFactory extends PushHandleStrategyFactory { 21 | @override 22 | Map get map => { 23 | 'type1': (payload) { 24 | final message = Message.fromMap(payload); 25 | return FirstStrategy(message); 26 | }, 27 | 'type2': (payload) { 28 | final message = Message.fromMap(payload); 29 | return SecondStrategy(message); 30 | }, 31 | }; 32 | 33 | @override 34 | StrategyBuilder get defaultStrategy { 35 | return (payload) => FirstStrategy( 36 | Message.fromMap(payload), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.2 4 | 5 | * Correct logo position in readme. 6 | 7 | ## 2.0.1 8 | 9 | * Rebranding. 10 | 11 | ## 2.0.0 12 | 13 | * Fix bug with dependencies resolving on android 14 | * update dependency constraints to sdk: '>=3.0.0 <4.0.0' flutter: '>=3.0.0' 15 | * update minimum surf_lint_rules version to 3.0.0 16 | * `_getStrategyBuilder` in `PushHandleStrategyFactory` now processes `messageData` the same on both ios and android 17 | * `enum MessageHandlerType` has been updated to reflect the changed message handlers in FirebaseMessaging. `onLaunch` and `onResume` removed, `onBackgroundMessage` and `onMessageOpenedApp` added. 18 | * fixed parsing error in the `internalOnSelectNotification` method in `NotificationController` 19 | * `handleMessage` in `PushHandler` method has been updated to match the updated `MessageHandlerType` 20 | * fixed the problem of incorrect display of push notifications on iOS 21 | * updated usage example 22 | * updated docs: no longer need to specify `click_action: FLUTTER_NOTIFICATION_CLICK` in notification and add `intent-filter` to android manifest 23 | 24 | ## 1.1.1 25 | 26 | * v2 embedded support 27 | * internal improvement 28 | 29 | ## 1.1.0 30 | 31 | * Stable release 32 | 33 | ## 1.0.1-dev.1 34 | 35 | * Update `rxdart` dependency to `0.27.0`. 36 | 37 | ## 1.0.0 38 | 39 | * Migrated to null safety, min SDK is `2.12.0`. 40 | 41 | ## 0.0.1-dev.6 42 | 43 | * now delegate implements in show instead of requestPermission 44 | 45 | ## 0.0.1-dev.3 46 | 47 | * Fix lint hints 48 | 49 | ## 0.0.1-dev.0 50 | 51 | * Initial release 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | 35 | # FVM 36 | .fvm/ 37 | 38 | # Android related 39 | **/android/**/gradle-wrapper.jar 40 | **/android/.gradle 41 | **/android/captures/ 42 | **/android/gradlew 43 | **/android/gradlew.bat 44 | **/android/local.properties 45 | **/android/**/GeneratedPluginRegistrant.java 46 | 47 | # iOS/XCode related 48 | **/ios/**/*.mode1v3 49 | **/ios/**/*.mode2v3 50 | **/ios/**/*.moved-aside 51 | **/ios/**/*.pbxuser 52 | **/ios/**/*.perspectivev3 53 | **/ios/**/*sync/ 54 | **/ios/**/.sconsign.dblite 55 | **/ios/**/.tags* 56 | **/ios/**/.vagrant/ 57 | **/ios/**/DerivedData/ 58 | **/ios/**/Icon? 59 | **/ios/**/Pods/ 60 | **/ios/**/.symlinks/ 61 | **/ios/**/profile 62 | **/ios/**/xcuserdata 63 | **/ios/.generated/ 64 | **/ios/Flutter/App.framework 65 | **/ios/Flutter/Flutter.framework 66 | **/ios/Flutter/Generated.xcconfig 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/Flutter/app.flx 69 | **/ios/Flutter/app.zip 70 | **/ios/Flutter/flutter_assets/ 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Coverage 75 | coverage/ 76 | 77 | # Exceptions to above rules. 78 | !**/ios/**/default.mode1v3 79 | !**/ios/**/default.mode2v3 80 | !**/ios/**/default.pbxuser 81 | !**/ios/**/default.perspectivev3 82 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/notification/notificator/android/android_notiffication_specifics.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Android notification settings. 16 | class AndroidNotificationSpecifics { 17 | /// Icon drawable. 18 | /// 19 | /// @mipmap/ic_launcher 20 | final String? icon; 21 | 22 | /// Channel id. 23 | /// 24 | /// @string/notification_channel_id 25 | final String? channelId; 26 | 27 | /// Channel name. 28 | /// 29 | /// @string/notification_channel_name 30 | final String? channelName; 31 | 32 | /// Icon color. 33 | /// 34 | /// @color/notification_color 35 | final String? color; 36 | 37 | /// Notification is auto cancel. 38 | final bool? autoCancelable; 39 | 40 | AndroidNotificationSpecifics({ 41 | this.icon, 42 | this.channelId, 43 | this.channelName, 44 | this.color, 45 | this.autoCancelable, 46 | }); 47 | 48 | Map toMap() { 49 | return { 50 | 'icon': icon, 51 | 'channelId': channelId, 52 | 'channelName': channelName, 53 | 'color': color, 54 | 'autoCancelable': autoCancelable, 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | compileSdkVersion 34 27 | 28 | sourceSets { 29 | main.java.srcDirs += 'src/main/kotlin' 30 | } 31 | 32 | lintOptions { 33 | disable 'InvalidPackage' 34 | } 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId "com.example.push_demo" 39 | minSdkVersion 19 40 | targetSdkVersion 30 41 | versionCode flutterVersionCode.toInteger() 42 | versionName flutterVersionName 43 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 44 | multiDexEnabled true 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source '../..' 58 | } 59 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | .fvm/flutter_sdk 35 | 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # iOS/XCode related 46 | **/ios/**/*.mode1v3 47 | **/ios/**/*.mode2v3 48 | **/ios/**/*.moved-aside 49 | **/ios/**/*.pbxuser 50 | **/ios/**/*.perspectivev3 51 | **/ios/**/*sync/ 52 | **/ios/**/.sconsign.dblite 53 | **/ios/**/.tags* 54 | **/ios/**/.vagrant/ 55 | **/ios/**/DerivedData/ 56 | **/ios/**/Icon? 57 | **/ios/**/Pods/ 58 | **/ios/**/.symlinks/ 59 | **/ios/**/profile 60 | **/ios/**/xcuserdata 61 | **/ios/.generated/ 62 | **/ios/Flutter/App.framework 63 | **/ios/Flutter/Flutter.framework 64 | **/ios/Flutter/Generated.xcconfig 65 | **/ios/Flutter/flutter_export_environment.sh 66 | **/ios/Flutter/app.flx 67 | **/ios/Flutter/app.zip 68 | **/ios/Flutter/flutter_assets/ 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Coverage 73 | coverage/ 74 | 75 | # Exceptions to above rules. 76 | !**/ios/**/default.mode1v3 77 | !**/ios/**/default.mode2v3 78 | !**/ios/**/default.pbxuser 79 | !**/ios/**/default.perspectivev3 80 | 81 | # Scripts 82 | /scripts/google-services.json 83 | /scripts/node_modules/ 84 | 85 | # Firebase 86 | /lib/firebase_options.dart 87 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/push_observer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/cupertino.dart'; 16 | import 'package:flutter_test/flutter_test.dart'; 17 | import 'package:mocktail/mocktail.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | void main() { 21 | late PushObserver pushObserver; 22 | late MockRoute route; 23 | late MockRoute previousRoute; 24 | 25 | setUp( 26 | () { 27 | pushObserver = PushObserver(); 28 | route = MockRoute(); 29 | previousRoute = MockRoute(); 30 | }, 31 | ); 32 | 33 | test( 34 | 'After calling the didPush method, PushNavigatorHolder().navigator must ' 35 | 'be equal NavigatorObserver().navigator', 36 | () { 37 | pushObserver.didPush(route, previousRoute); 38 | 39 | expect( 40 | PushNavigatorHolder().navigator == NavigatorObserver().navigator, 41 | true, 42 | ); 43 | }, 44 | ); 45 | 46 | test( 47 | 'After calling the didPop method, PushNavigatorHolder().navigator must ' 48 | 'be equal NavigatorObserver().navigator', 49 | () { 50 | pushObserver.didPop(route, previousRoute); 51 | 52 | expect( 53 | PushNavigatorHolder().navigator == NavigatorObserver().navigator, 54 | true, 55 | ); 56 | }, 57 | ); 58 | } 59 | 60 | class MockRoute extends Mock implements Route {} 61 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FirebaseAppDelegateProxyEnabled 6 | 7 | CADisableMinimumFrameDurationOnPhone 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | push_demo 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UIBackgroundModes 32 | 33 | fetch 34 | processing 35 | remote-notification 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/lib/ui/message_list.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | import 'package:push_notification/push_notification.dart'; 18 | 19 | /// Listens for incoming foreground messages and displays them in a list. 20 | class MessageList extends StatefulWidget { 21 | final PushHandler pushHandler; 22 | const MessageList({required this.pushHandler, super.key}); 23 | 24 | @override 25 | State createState() => _MessageList(); 26 | } 27 | 28 | class _MessageList extends State { 29 | List _messages = []; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | widget.pushHandler.messageSubject.listen((messageMap) { 35 | final message = Message.fromMap(messageMap); 36 | setState(() { 37 | _messages = [..._messages, message]; 38 | }); 39 | }); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | if (_messages.isEmpty) { 45 | return const Text('No messages received'); 46 | } 47 | 48 | return ListView.builder( 49 | shrinkWrap: true, 50 | itemCount: _messages.length, 51 | physics: const NeverScrollableScrollPhysics(), 52 | padding: EdgeInsets.zero, 53 | itemBuilder: (context, index) { 54 | final message = _messages[index]; 55 | 56 | return ListTile( 57 | title: Text(message.title), 58 | subtitle: Text(message.body), 59 | ); 60 | }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/base/push_handle_strategy_factory.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/cupertino.dart'; 16 | import 'package:push_notification/src/base/push_handle_strategy.dart'; 17 | import 'package:push_notification/src/util/platform_wrapper.dart'; 18 | 19 | /// Strategy builder function. 20 | typedef StrategyBuilder = PushHandleStrategy Function( 21 | Map payload, 22 | ); 23 | 24 | /// Abstract factory for push notification strategies. 25 | abstract class PushHandleStrategyFactory { 26 | /// Action key in data firebase's push. 27 | /// You can customize your format in the factory implementation. 28 | static const _key = 'event'; 29 | 30 | /// Wrapper for Platform io. 31 | @visibleForTesting 32 | final PlatformWrapper platform; 33 | 34 | /// Default strategy, if in the notification is no strategy information. 35 | StrategyBuilder get defaultStrategy; 36 | 37 | /// Override with the necessary matching actions and strategy builder. 38 | Map get map => {}; 39 | 40 | PushHandleStrategyFactory({PlatformWrapper? platformWrapper}) 41 | : platform = platformWrapper ?? PlatformWrapper(); 42 | 43 | /// Returns a strategy from push data. 44 | PushHandleStrategy createByData(Map messageData) { 45 | StrategyBuilder? builder; 46 | try { 47 | builder = _getStrategyBuilder(messageData); 48 | 49 | return builder!(messageData); 50 | } on Exception catch (e) { 51 | // ignore: avoid_print 52 | print('$e - cant found $_key'); 53 | return defaultStrategy(messageData); 54 | } 55 | } 56 | 57 | StrategyBuilder? _getStrategyBuilder(Map messageData) { 58 | final value = map[(messageData['data'] as Map)[_key]]; 59 | 60 | if (value != null) { 61 | return value; 62 | } else { 63 | throw Exception('Other type expected'); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/notification/example_factory.dart'; 17 | import 'package:push_demo/notification/messaging_service.dart'; 18 | import 'package:push_demo/ui/app.dart'; 19 | import 'package:push_notification/push_notification.dart'; 20 | 21 | /// To run the example, follow these steps: 22 | /// 1. Setup your app following this guide: https://firebase.google.com/docs/cloud-messaging/flutter/client#platform-specific_setup_and_requirements. 23 | /// 2. Run `flutterfire configure` in the example/ directory to setup your app with your Firebase project. 24 | /// 3. Run the app on an actual device for iOS, android is fine to run on an emulator. 25 | /// 4. Download a service account key (JSON file) from your Firebase console, rename it to "google-services.json" and add to the example/scripts directory. 26 | /// 5. Copy the token from the console or from the screen and place it in the `token` variable on line 7 in the `send-message.dart` file. 27 | /// 6. From your terminal, root to example/scripts directory & run `npm install`. 28 | /// 7. Run `node send-message.js ` in the example/scripts directory and your app will receive messages in any state; foreground, background, terminated. can be `type1` or `type2`. 29 | /// Note: Flutter API documentation for receiving messages: https://firebase.google.com/docs/cloud-messaging/flutter/receive 30 | /// 31 | Future main() async { 32 | WidgetsFlutterBinding.ensureInitialized(); 33 | // TODO(anyone): Uncomment after the firebase_option file is added. 34 | // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 35 | 36 | final messagingService = MessagingService(); 37 | 38 | final pushHandler = PushHandler( 39 | ExampleFactory(), 40 | NotificationController( 41 | () => debugPrint('permission decline'), 42 | ), 43 | messagingService, 44 | ); 45 | 46 | runApp(MyApp(pushHandler, messagingService)); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/base/push_handle_strategy.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/cupertino.dart'; 16 | import 'package:push_notification/src/base/notification_payload.dart'; 17 | 18 | /// Abstract notification processing strategy. 19 | abstract class PushHandleStrategy { 20 | /// Notification payload. 21 | final PT payload; 22 | 23 | /// Android notification channel id. 24 | /// 25 | /// "@string/notification_channel_id". 26 | String? notificationChannelId; 27 | 28 | /// Android notification channel name 29 | /// 30 | /// "@string/notification_channel_name". 31 | String? notificationChannelName; 32 | 33 | /// Push id. 34 | int pushId = 0; 35 | 36 | /// Auto close notification. 37 | bool autoCancelable = false; 38 | 39 | /// Path to string resource color notification icons 40 | /// "@color/notificaion_icon_color_name". 41 | String? color; 42 | 43 | /// Path to string resource notification icons 44 | /// "@mipmap/notificaion_icon_name". 45 | String? icon; 46 | 47 | /// Non-removable notification. 48 | /// Android only. 49 | bool ongoing = false; 50 | 51 | /// Indicates if a sound should be played when the notification is displayed. 52 | bool playSound = true; 53 | 54 | /// Display an alert when the notification is triggered while app is in the 55 | /// foreground. 56 | /// iOS 10+ only. 57 | bool presentAlert = true; 58 | 59 | PushHandleStrategy(this.payload); 60 | 61 | @override 62 | String toString() { 63 | return 'PushHandleStrategy{notificationChannelId: $notificationChannelId,' 64 | ' notificationChannelName: $notificationChannelName, pushId: $pushId,' 65 | ' autoCancelable: $autoCancelable, color: $color, icon: $icon, ongoing:' 66 | ' $ongoing, playSound: $playSound, presentAlert: $presentAlert,' 67 | ' payload: $payload}'; 68 | } 69 | 70 | /// Function that is called to process notification clicks. 71 | void onTapNotification(NavigatorState? navigator); 72 | 73 | /// Function that is called to process notification background. 74 | void onBackgroundProcess(Map message); 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/notification/notificator/android/android_notification.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/services.dart'; 16 | import 'package:flutter/widgets.dart'; 17 | import 'package:push_notification/src/notification/notificator/android/android_notiffication_specifics.dart'; 18 | import 'package:push_notification/src/notification/notificator/notificator.dart'; 19 | 20 | /// Notifications for the Android platform. 21 | class AndroidNotification { 22 | /// MethodChannel for connecting to android native code. 23 | final MethodChannel channel; 24 | 25 | /// Callback notification push. 26 | final OnNotificationTapCallback onNotificationTap; 27 | 28 | AndroidNotification({ 29 | required this.channel, 30 | required this.onNotificationTap, 31 | }); 32 | 33 | /// Initialize notification. 34 | /// 35 | /// Initializes notification parameters and listening clicks. 36 | Future init() async { 37 | channel.setMethodCallHandler( 38 | methodCallHandlerCallback, 39 | ); 40 | return channel.invokeMethod(callInit); 41 | } 42 | 43 | /// Show notification. 44 | /// 45 | /// [id] - notification identifier. 46 | /// [title] - title. 47 | /// [body] - the main text of the notification. 48 | /// [data] - data for notification. 49 | /// [notificationSpecifics] - notification specifics. 50 | Future show( 51 | int id, 52 | String title, 53 | String body, 54 | String? imageUrl, 55 | Map? data, 56 | AndroidNotificationSpecifics? notificationSpecifics, 57 | ) async { 58 | return channel.invokeMethod( 59 | callShow, 60 | { 61 | pushIdArg: id, 62 | titleArg: title, 63 | bodyArg: body, 64 | imageUrlArg: imageUrl, 65 | dataArg: data, 66 | notificationSpecificsArg: notificationSpecifics?.toMap(), 67 | }, 68 | ); 69 | } 70 | 71 | @visibleForTesting 72 | Future methodCallHandlerCallback(MethodCall call) async { 73 | switch (call.method) { 74 | case openCallback: 75 | final notificationData = call.arguments as Map; 76 | onNotificationTap(notificationData); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Push Notification 2 | 3 | 4 | 5 | 6 | [![Build Status](https://shields.io/github/actions/workflow/status/surfstudio/flutter-push-notification/on_pull_request.yml?logo=github&logoColor=white)](https://github.com/surfstudio/flutter-push-notification) 7 | [![Coverage Status](https://img.shields.io/codecov/c/github/surfstudio/flutter-push-notification?logo=codecov&logoColor=white)](https://app.codecov.io/gh/surfstudio/flutter-push-notification) 8 | [![Pub Version](https://img.shields.io/pub/v/push_notification?logo=dart&logoColor=white)](https://pub.dev/packages/push_notification) 9 | [![Pub Likes](https://badgen.net/pub/likes/push_notification)](https://pub.dev/packages/push_notification) 10 | [![Pub popularity](https://badgen.net/pub/popularity/push_notification)](https://pub.dev/packages/push_notification/score) 11 | ![Flutter Platform](https://badgen.net/pub/flutter-platform/push_notification) 12 | 13 | ## Overview 14 | 15 | Library for implementing push notifications. 16 | The module contains the main work with push notifications. 17 | 18 | ## Example 19 | 20 | An example of using the library can be found in [example](example). 21 | 22 | * Create a notification data type through inheritance `NotificationPayload`. 23 | * Create a strategy for handling notifications through inheritance `PushHandleStrategy`. 24 | * Create a factory of strategies through inheritance `PushHandleStrategyFactory`. 25 | 26 | * To receive notifications, you need to create an instance. `MessagingService`. 27 | * To display notifications, you need to create an instance `NotificationController`. 28 | * And pass created instances when creating `PushHandler` that will create the strategy using the factory. 29 | 30 | ## Installation 31 | 32 | Add `push_notification` to your `pubspec.yaml` file: 33 | 34 | ```yaml 35 | dependencies: 36 | push_notification: $currentVersion$ 37 | ``` 38 | 39 |

At this moment, the current version of push_notification is push_notification version.

40 | 41 | ## Changelog 42 | 43 | All notable changes to this project will be documented in [this file](./CHANGELOG.md). 44 | 45 | ## Issues 46 | 47 | To report your issues, file directly in the [Issues](https://github.com/surfstudio/flutter-push-notification/issues) section. 48 | 49 | ## Contribute 50 | 51 | If you would like to contribute to the package (e.g. by improving the documentation, fixing a bug or adding a cool new feature), please read our [contribution guide](./CONTRIBUTING.md) first and send us your pull request. 52 | 53 | Your PRs are always welcome. 54 | 55 | ## How to reach us 56 | 57 | Please feel free to ask any questions about this package. Join our community chat on Telegram. We speak English and Russian. 58 | 59 | [![Telegram](https://img.shields.io/badge/chat-on%20Telegram-blue.svg)](https://t.me/SurfGear) 60 | 61 | ## License 62 | 63 | [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 64 | -------------------------------------------------------------------------------- /lib/src/notification/notificator/ios/ios_notification.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/cupertino.dart'; 16 | import 'package:flutter/services.dart'; 17 | import 'package:push_notification/src/notification/notificator/ios/ios_notification_specifics.dart'; 18 | import 'package:push_notification/src/notification/notificator/notificator.dart'; 19 | 20 | /// Notifications for the iOS platform. 21 | class IOSNotification { 22 | /// MethodChannel for connecting to iOS native platform. 23 | final MethodChannel channel; 24 | 25 | /// Callback notification push. 26 | final OnNotificationTapCallback onNotificationTap; 27 | 28 | /// Callback on notification decline. 29 | final OnPermissionDeclineCallback? onPermissionDecline; 30 | 31 | IOSNotification({ 32 | required this.channel, 33 | required this.onNotificationTap, 34 | this.onPermissionDecline, 35 | }); 36 | 37 | /// Initialize notification. 38 | /// 39 | /// Initializes notification parameters. 40 | Future init() async { 41 | channel.setMethodCallHandler( 42 | methodCallHandlerCallback, 43 | ); 44 | } 45 | 46 | /// Request permissions. 47 | /// 48 | /// [requestSoundPermission] - is play sound. 49 | /// [requestAlertPermission] - is show alert. 50 | Future requestPermissions({ 51 | bool? requestSoundPermission, 52 | bool? requestAlertPermission, 53 | }) => 54 | channel.invokeMethod( 55 | callRequest, 56 | { 57 | 'requestAlertPermission': requestAlertPermission ?? false, 58 | 'requestSoundPermission': requestSoundPermission ?? false, 59 | }, 60 | ); 61 | 62 | /// Show notification. 63 | /// 64 | /// [id] - notification identifier. 65 | /// [title] - title. 66 | /// [body] - the main text of the notification. 67 | /// [data] - data for notification. 68 | /// [notificationSpecifics] - notification specifics. 69 | Future show( 70 | int id, 71 | String title, 72 | String body, 73 | String? imageUrl, 74 | Map? data, 75 | IosNotificationSpecifics? notificationSpecifics, 76 | ) => 77 | channel.invokeMethod( 78 | callShow, 79 | { 80 | pushIdArg: id, 81 | titleArg: title, 82 | bodyArg: body, 83 | imageUrlArg: imageUrl, 84 | dataArg: data, 85 | }, 86 | ); 87 | 88 | @visibleForTesting 89 | Future methodCallHandlerCallback(MethodCall call) async { 90 | switch (call.method) { 91 | case openCallback: 92 | onNotificationTap(call.arguments as Map); 93 | case permissionDeclineCallback: 94 | onPermissionDecline?.call(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /android/src/main/kotlin/pushnotification/push_notification/strategy/PushStrategy.kt: -------------------------------------------------------------------------------- 1 | package pushnotification.push_notification.strategy 2 | 3 | import android.app.Activity 4 | import android.app.NotificationChannel 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.graphics.Bitmap 8 | import android.graphics.BitmapFactory 9 | import android.widget.RemoteViews 10 | import androidx.core.app.NotificationCompat 11 | import androidx.core.content.ContextCompat 12 | import ru.surfstudio.android.notification.ui.notification.strategies.PushHandleStrategy 13 | import pushnotification.push_notification.type.PushNotificationTypeData 14 | import java.io.IOException 15 | import java.net.HttpURLConnection 16 | import java.net.URL 17 | 18 | /** Push strategy for [PushHandler]**/ 19 | class PushStrategy(override val icon: Int, 20 | override val channelId: Int, 21 | override val color: Int, 22 | override val autoCancelable: Boolean, 23 | override val channelName: Int, 24 | private val imageUrl: String? 25 | ) : PushHandleStrategy() { 26 | 27 | override val typeData by lazy { PushNotificationTypeData() } 28 | 29 | override val contentView: RemoteViews? = null 30 | 31 | override fun makeNotificationBuilder(context: Context, title: String, body: String): NotificationCompat.Builder? { 32 | return NotificationCompat.Builder(context, context.getString(channelId)) 33 | .setContentTitle(title) 34 | .setContentText(body) 35 | .setSmallIcon(icon) 36 | .setColor(ContextCompat.getColor(context, color)) 37 | .setAutoCancel(autoCancelable) 38 | .setGroup(group?.groupAlias) 39 | .setContentIntent(pendingIntent) 40 | .applyLargeIcon(imageUrl) 41 | 42 | } 43 | 44 | override fun makeNotificationChannel(context: Context, title: String): NotificationChannel? = null 45 | 46 | override fun coldStartIntent(context: Context): Intent? { 47 | return null 48 | } 49 | 50 | override fun handlePushInActivity(activity: Activity): Boolean = false 51 | 52 | } 53 | 54 | private fun NotificationCompat.Builder.applyLargeIcon(imageUrl: String?): NotificationCompat.Builder = apply { 55 | if (imageUrl != null && imageUrl.isNotEmpty()) { 56 | try { 57 | val bitmap = getBitmapFromURL(imageUrl) 58 | if (bitmap != null) { 59 | val bigPictureStyle = NotificationCompat.BigPictureStyle() 60 | .bigPicture(bitmap) 61 | .bigLargeIcon(null) // Hide small icon and show image 62 | setStyle(bigPictureStyle) 63 | setLargeIcon(bitmap) 64 | } 65 | } catch (e: Exception) { 66 | print("Error while downloading image") 67 | } 68 | 69 | } 70 | } 71 | 72 | private fun getBitmapFromURL(src: String?): Bitmap? { 73 | return try { 74 | val url = URL(src) 75 | val connection = url.openConnection() as HttpURLConnection 76 | connection.doInput = true 77 | connection.connect() 78 | val input = connection.inputStream 79 | BitmapFactory.decodeStream(input) 80 | } catch (e: IOException) { // Log exception 81 | null 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/push_handler.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/cupertino.dart'; 16 | import 'package:push_notification/push_notification.dart'; 17 | import 'package:push_notification/src/util/platform_wrapper.dart'; 18 | import 'package:rxdart/subjects.dart'; 19 | 20 | typedef HandleMessageFunction = void Function( 21 | Map message, MessageHandlerType handlerType); 22 | 23 | /// Notification handling. 24 | class PushHandler { 25 | /// The ability to directly subscribe to receive messages. 26 | final PublishSubject> messageSubject = PublishSubject(); 27 | 28 | final BehaviorSubject selectNotificationSubject = 29 | BehaviorSubject(); 30 | 31 | @visibleForTesting 32 | final PlatformWrapper platform; 33 | 34 | final PushHandleStrategyFactory _strategyFactory; 35 | final NotificationController _notificationController; 36 | final BaseMessagingService _messagingService; 37 | 38 | PushHandler( 39 | this._strategyFactory, 40 | this._notificationController, 41 | this._messagingService, { 42 | PlatformWrapper? platform, 43 | }) : platform = platform ?? PlatformWrapper() { 44 | _messagingService.initNotification(handleMessage); 45 | } 46 | 47 | /// Request permission for show notification. 48 | /// [soundPemission] - is play sound. 49 | /// [alertPermission] - is show alert. 50 | Future requestPermissions({ 51 | bool? soundPemission, 52 | bool? alertPermission, 53 | }) { 54 | if (platform.isIOS) { 55 | return _notificationController.requestPermissions( 56 | requestSoundPermission: soundPemission, 57 | requestAlertPermission: alertPermission, 58 | ); 59 | } else { 60 | return Future.value(); 61 | } 62 | } 63 | 64 | /// Display local notification. 65 | /// MessagingService calls this method to handle the notification that 66 | /// came from message service. 67 | void handleMessage( 68 | Map message, 69 | MessageHandlerType handlerType, { 70 | bool localNotification = false, 71 | }) { 72 | if (!localNotification && 73 | handlerType != MessageHandlerType.onMessageOpenedApp) { 74 | messageSubject.add(message); 75 | } 76 | 77 | final strategy = _strategyFactory.createByData(message); 78 | 79 | switch (handlerType) { 80 | case MessageHandlerType.onMessage: 81 | _notificationController.show( 82 | strategy, 83 | (_) { 84 | selectNotificationSubject.add(strategy); 85 | strategy.onTapNotification(PushNavigatorHolder().navigator); 86 | }, 87 | ); 88 | case MessageHandlerType.onBackgroundMessage: 89 | strategy.onBackgroundProcess(message); 90 | 91 | case MessageHandlerType.onMessageOpenedApp: 92 | selectNotificationSubject.add(strategy); 93 | strategy.onTapNotification(PushNavigatorHolder().navigator); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /example/lib/notification/messaging_service.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:firebase_messaging/firebase_messaging.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | import 'package:push_demo/utils/logger.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | 20 | /// Wrapper over [FirebaseMessaging]. 21 | class MessagingService extends BaseMessagingService { 22 | final FirebaseMessaging _messaging = FirebaseMessaging.instance; 23 | final List _topicsSubscription = []; 24 | late HandleMessageFunction _handleMessage; 25 | 26 | Future get token => _messaging.getToken(); 27 | 28 | Future get initialMessage async { 29 | final remoteMessage = await _messaging.getInitialMessage(); 30 | 31 | if (remoteMessage == null) return null; 32 | 33 | return Message.fromMap(remoteMessage.toMap()); 34 | } 35 | 36 | /// No need to call. Initialization is called inside the [PushHandler]. 37 | @override 38 | void initNotification(HandleMessageFunction handleMessage) { 39 | _handleMessage = handleMessage; 40 | 41 | FirebaseMessaging.onMessage.listen(_foregroundMessageHandler); 42 | 43 | FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler); 44 | 45 | FirebaseMessaging.onMessageOpenedApp.listen(_messageOpenedAppHandler); 46 | } 47 | 48 | void _foregroundMessageHandler(RemoteMessage message) { 49 | logger.d('FIREBASE FOREGROUND MESSAGE: ${message.toMap()}'); 50 | _handleMessage.call(message.toMap(), MessageHandlerType.onMessage); 51 | } 52 | 53 | @pragma('vm:entry-point') 54 | static Future _backgroundMessageHandler(RemoteMessage message) async { 55 | logger.d('FIREBASE BACKGROUND MESSAGE: ${message.toMap()}'); 56 | } 57 | 58 | void _messageOpenedAppHandler(RemoteMessage message) { 59 | logger.d('FIREBASE MESSAGE OPENED APP: ${message.toMap()}'); 60 | _handleMessage.call(message.toMap(), MessageHandlerType.onMessageOpenedApp); 61 | } 62 | 63 | /// Request notification permissions for iOS platform. 64 | void requestNotificationPermissions() { 65 | _messaging.requestPermission(); 66 | } 67 | 68 | /// Subscribe to [topic] in background. 69 | void subscribeToTopic(String topic) { 70 | _messaging.subscribeToTopic(topic); 71 | _topicsSubscription.add(topic); 72 | } 73 | 74 | /// Subscribe on a list of [topics] in background. 75 | void subscribeToListTopics(List topics) { 76 | topics.forEach(subscribeToTopic); 77 | } 78 | 79 | /// Unsubscribe from [topic] in background. 80 | void unsubscribeFromTopic(String topic) { 81 | _messaging.unsubscribeFromTopic(topic); 82 | _topicsSubscription.remove(topic); 83 | } 84 | 85 | /// Unsubscribe from list of [topics]. 86 | void unsubscribeFromListTopics(List topics) { 87 | topics.forEach(unsubscribeFromTopic); 88 | } 89 | 90 | /// Unsubscribe from all topics. 91 | void unsubscribe() { 92 | _topicsSubscription.forEach(unsubscribeFromTopic); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /test/push_handle_strategy_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // ignore_for_file: unreachable_from_main 16 | 17 | import 'package:flutter/src/widgets/navigator.dart'; 18 | import 'package:flutter_test/flutter_test.dart'; 19 | import 'package:push_notification/push_notification.dart'; 20 | 21 | void main() { 22 | late TestPushHandleStrategy pushHandleStrategy; 23 | late NotificationPayload payload; 24 | 25 | setUp( 26 | () { 27 | payload = TestNotificationPayload( 28 | {'Test data': 'Test data'}, 29 | 'Test title', 30 | 'Test body', 31 | 'Test url', 32 | ); 33 | 34 | pushHandleStrategy = TestPushHandleStrategy( 35 | payload: payload, 36 | notificationChannelId: '123', 37 | notificationChannelName: 'Test name', 38 | pushId: 5, 39 | autoCancelable: true, 40 | color: 'Test color', 41 | icon: 'Test icon', 42 | ongoing: true, 43 | playSound: true, 44 | presentAlert: true, 45 | ); 46 | }, 47 | ); 48 | 49 | test( 50 | 'Method toString should work correctly', 51 | () { 52 | const expectedResult = 53 | "PushHandleStrategy{notificationChannelId: 123, notificationChannelName: Test name, pushId: 5, autoCancelable: true, color: Test color, icon: Test icon, ongoing: true, playSound: true, presentAlert: true, payload: Instance of 'TestNotificationPayload'}"; 54 | 55 | final result = pushHandleStrategy.toString(); 56 | 57 | expect(result, expectedResult); 58 | }, 59 | ); 60 | } 61 | 62 | // ignore_for_file: overridden_fields 63 | class TestPushHandleStrategy extends PushHandleStrategy { 64 | @override 65 | NotificationPayload payload; 66 | @override 67 | String? notificationChannelId; 68 | @override 69 | String? notificationChannelName; 70 | @override 71 | int pushId; 72 | @override 73 | bool autoCancelable; 74 | @override 75 | String? color; 76 | @override 77 | String? icon; 78 | @override 79 | bool ongoing; 80 | @override 81 | bool playSound; 82 | @override 83 | bool presentAlert; 84 | 85 | TestPushHandleStrategy({ 86 | required this.payload, 87 | required this.notificationChannelId, 88 | required this.notificationChannelName, 89 | required this.pushId, 90 | required this.autoCancelable, 91 | required this.color, 92 | required this.icon, 93 | required this.ongoing, 94 | required this.playSound, 95 | required this.presentAlert, 96 | }) : super(payload); 97 | 98 | @override 99 | void onBackgroundProcess(Map message) {} 100 | 101 | @override 102 | void onTapNotification(NavigatorState? navigator) {} 103 | } 104 | 105 | class TestNotificationPayload extends NotificationPayload { 106 | final Map testMessageData; 107 | final String testTitle; 108 | final String testBody; 109 | final String? testImageUrl; 110 | 111 | TestNotificationPayload( 112 | this.testMessageData, 113 | this.testTitle, 114 | this.testBody, 115 | this.testImageUrl, 116 | ) : super( 117 | testMessageData, 118 | testTitle, 119 | testBody, 120 | imageUrl: testImageUrl, 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /test/android_notification_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/services.dart'; 16 | import 'package:flutter_test/flutter_test.dart'; 17 | import 'package:mocktail/mocktail.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | import 'package:push_notification/src/notification/notificator/android/android_notification.dart'; 20 | 21 | void main() { 22 | late AndroidNotification andtoidNotification; 23 | late MockMethodChannel channel; 24 | late MockOnNotificationTapFunction onNotificationTapMethod; 25 | 26 | setUp( 27 | () { 28 | channel = MockMethodChannel(); 29 | when(() => channel.setMethodCallHandler(any())) 30 | .thenAnswer((invocation) => Future.value()); 31 | when(() => channel.invokeMethod( 32 | any(), 33 | any(), 34 | )).thenAnswer((invocation) => Future.value()); 35 | 36 | onNotificationTapMethod = MockOnNotificationTapFunction(); 37 | when(() => onNotificationTapMethod.call(any())) 38 | .thenAnswer((invocation) => Future.value()); 39 | 40 | andtoidNotification = AndroidNotification( 41 | channel: channel, 42 | onNotificationTap: onNotificationTapMethod, 43 | ); 44 | }, 45 | ); 46 | 47 | test( 48 | 'When calling the init method, the channel.setMethodCallHandler and ' 49 | 'channel.invokeMothod methods must be called', 50 | () async { 51 | await andtoidNotification.init(); 52 | 53 | verify(() => channel.setMethodCallHandler(any())).called(1); 54 | verify(() => channel.invokeMethod(any())).called(1); 55 | }, 56 | ); 57 | 58 | test( 59 | 'with openCallback MethodCall, the onNotificationTapMethod method ' 60 | 'must be called once with correctly argument', 61 | () async { 62 | const methodCall = MethodCall(openCallback, {'Test arguments': 'Test'}); 63 | 64 | await andtoidNotification.methodCallHandlerCallback(methodCall); 65 | 66 | verify(() => onNotificationTapMethod(methodCall.arguments as Map)) 67 | .called(1); 68 | }, 69 | ); 70 | 71 | test( 72 | 'When calling the show method, the channel.invokeMethod must ' 73 | 'be called with correctly parameters', 74 | () async { 75 | const id = 1; 76 | const title = 'Test title'; 77 | const body = 'Test body'; 78 | const imageUrl = 'Test url'; 79 | const data = {'Test': 'Test strinf'}; 80 | final notificationSpecifics = AndroidNotificationSpecifics(); 81 | 82 | await andtoidNotification.show( 83 | id, 84 | title, 85 | body, 86 | imageUrl, 87 | data, 88 | notificationSpecifics, 89 | ); 90 | 91 | verify(() => channel.invokeMethod( 92 | callShow, 93 | { 94 | pushIdArg: id, 95 | titleArg: title, 96 | bodyArg: body, 97 | imageUrlArg: imageUrl, 98 | dataArg: data, 99 | notificationSpecificsArg: notificationSpecifics.toMap(), 100 | }, 101 | )).called(1); 102 | }, 103 | ); 104 | } 105 | 106 | class MockMethodChannel extends Mock implements MethodChannel {} 107 | 108 | class MockOnNotificationTapFunction extends Mock { 109 | void call(Map? notificationData); 110 | } 111 | -------------------------------------------------------------------------------- /example/lib/ui/main_screen.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/material.dart'; 16 | import 'package:push_demo/domain/message.dart'; 17 | import 'package:push_demo/notification/messaging_service.dart'; 18 | import 'package:push_demo/ui/message_list.dart'; 19 | import 'package:push_notification/push_notification.dart'; 20 | 21 | const String androidMipMapIcon = '@mipmap/ic_launcher'; 22 | 23 | class MainScreen extends StatefulWidget { 24 | final PushHandler pushHandler; 25 | final MessagingService messagingService; 26 | 27 | const MainScreen( 28 | {required this.pushHandler, required this.messagingService, super.key}); 29 | 30 | @override 31 | MainScreenState createState() => MainScreenState(); 32 | } 33 | 34 | class MainScreenState extends State { 35 | String? _token; 36 | Message? _initialMessage; 37 | 38 | @override 39 | void initState() { 40 | widget.pushHandler 41 | .requestPermissions(soundPemission: true, alertPermission: true); 42 | 43 | widget.messagingService.token.then( 44 | (value) { 45 | debugPrint('FCM Token: $value'); 46 | setState(() { 47 | _token = value; 48 | }); 49 | }, 50 | ); 51 | 52 | widget.messagingService.initialMessage.then( 53 | (value) => setState( 54 | () => _initialMessage = value, 55 | ), 56 | ); 57 | 58 | super.initState(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Scaffold( 64 | appBar: AppBar( 65 | title: const Text('Push demo'), 66 | ), 67 | body: SingleChildScrollView( 68 | child: Column( 69 | children: [ 70 | _DataCard( 71 | 'FCM Token', 72 | _token == null 73 | ? const CircularProgressIndicator() 74 | : SelectableText(_token!, 75 | style: const TextStyle(fontSize: 12)), 76 | ), 77 | _DataCard( 78 | 'Initial Message', 79 | _initialMessage == null 80 | ? const Text('None') 81 | : Column( 82 | children: [ 83 | Text('Title : ${_initialMessage!.title}'), 84 | Text('Body: ${_initialMessage!.body}'), 85 | ], 86 | ), 87 | ), 88 | _DataCard( 89 | 'Message Stream', MessageList(pushHandler: widget.pushHandler)), 90 | ], 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | 97 | /// Widget for displaying data. 98 | class _DataCard extends StatelessWidget { 99 | final String _title; 100 | final Widget _children; 101 | 102 | const _DataCard(this._title, this._children); 103 | 104 | @override 105 | Widget build(BuildContext context) { 106 | return Padding( 107 | padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), 108 | child: SizedBox( 109 | width: double.infinity, 110 | child: Card( 111 | child: Padding( 112 | padding: const EdgeInsets.all(16), 113 | child: Column( 114 | children: [ 115 | Text(_title, style: const TextStyle(fontSize: 18)), 116 | const SizedBox(height: 16), 117 | _children, 118 | ], 119 | ), 120 | ), 121 | ), 122 | ), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/src/notification/notification_controller.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:collection'; 16 | 17 | import 'package:flutter/cupertino.dart'; 18 | import 'package:push_notification/src/base/push_handle_strategy.dart'; 19 | import 'package:push_notification/src/notification/notificator/android/android_notiffication_specifics.dart'; 20 | import 'package:push_notification/src/notification/notificator/notification_specifics.dart'; 21 | import 'package:push_notification/src/notification/notificator/notificator.dart'; 22 | 23 | typedef NotificationCallback = void Function(Map payload); 24 | 25 | const String pushIdParam = 'localPushId'; 26 | 27 | /// Wrapper over surf notifications. 28 | class NotificationController { 29 | Map callbackMap = 30 | HashMap(); 31 | 32 | @visibleForTesting 33 | late Notificator notificator; 34 | 35 | NotificationController( 36 | OnPermissionDeclineCallback onPermissionDecline, { 37 | Notificator? transmittedNotificator, 38 | }) { 39 | notificator = transmittedNotificator ?? 40 | Notificator( 41 | onNotificationTapCallback: internalOnSelectNotification, 42 | onPermissionDecline: onPermissionDecline, 43 | ); 44 | } 45 | 46 | /// Request notification permissions (iOS only). 47 | Future requestPermissions({ 48 | bool? requestSoundPermission, 49 | bool? requestAlertPermission, 50 | }) { 51 | return notificator.requestPermissions( 52 | requestSoundPermission: requestSoundPermission, 53 | requestAlertPermission: requestAlertPermission, 54 | ); 55 | } 56 | 57 | /// Displaying notification from the strategy. 58 | Future show( 59 | PushHandleStrategy strategy, 60 | NotificationCallback onSelectNotification, 61 | ) { 62 | final androidSpecifics = AndroidNotificationSpecifics( 63 | channelId: strategy.notificationChannelId, 64 | channelName: strategy.notificationChannelName, 65 | autoCancelable: strategy.autoCancelable, 66 | color: strategy.color, 67 | icon: strategy.icon, 68 | ); 69 | 70 | final platformSpecifics = NotificationSpecifics(androidSpecifics); 71 | 72 | // ignore: avoid_print 73 | print( 74 | 'DEV_INFO receive for show push : ${strategy.payload.title}, ' 75 | '${strategy.payload.body}', 76 | ); 77 | 78 | final pushId = DateTime.now().millisecondsSinceEpoch; 79 | 80 | final tmpPayload = strategy.payload.messageData.map( 81 | // ignore: avoid_annotating_with_dynamic 82 | (key, dynamic value) => MapEntry( 83 | key, 84 | value.toString(), 85 | ), 86 | ); 87 | 88 | tmpPayload[pushIdParam] = '$pushId'; 89 | callbackMap[pushId] = onSelectNotification; 90 | 91 | return notificator.show( 92 | strategy.pushId, 93 | strategy.payload.title, 94 | strategy.payload.body, 95 | imageUrl: strategy.payload.imageUrl, 96 | data: tmpPayload, 97 | notificationSpecifics: platformSpecifics, 98 | ); 99 | } 100 | 101 | @visibleForTesting 102 | void internalOnSelectNotification(Map? payload) { 103 | // ignore: avoid_print 104 | print('DEV_INFO onSelectNotification, payload: $payload'); 105 | 106 | if (payload != null) { 107 | final tmpPayload = payload.cast(); 108 | final pushId = int.tryParse(tmpPayload[pushIdParam]!); 109 | // ignore: avoid_print 110 | print('DEV_INFO $pushId'); 111 | final onSelectNotification = callbackMap[pushId]; 112 | callbackMap.remove(pushId); 113 | 114 | onSelectNotification?.call(tmpPayload); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/notification/notificator/notificator.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | 17 | import 'package:flutter/foundation.dart'; 18 | import 'package:flutter/services.dart'; 19 | import 'package:push_notification/src/notification/notificator/android/android_notification.dart'; 20 | import 'package:push_notification/src/notification/notificator/ios/ios_notification.dart'; 21 | import 'package:push_notification/src/notification/notificator/notification_specifics.dart'; 22 | import 'package:push_notification/src/util/platform_wrapper.dart'; 23 | 24 | /// Callback notification clicks. 25 | /// 26 | /// [notificationData] - notification data. 27 | typedef OnNotificationTapCallback = void Function( 28 | Map notificationData); 29 | 30 | /// Callback on permission decline. 31 | typedef OnPermissionDeclineCallback = void Function(); 32 | 33 | /// Channel name. 34 | const String channelName = 'surf_notification'; 35 | 36 | /// Methods names. 37 | const String callInit = 'initialize'; 38 | const String callShow = 'show'; 39 | const String callRequest = 'request'; 40 | const String openCallback = 'notificationOpen'; 41 | const String permissionDeclineCallback = 'permissionDecline'; 42 | 43 | /// Arguments names. 44 | const String pushIdArg = 'pushId'; 45 | const String titleArg = 'title'; 46 | const String bodyArg = 'body'; 47 | const String imageUrlArg = 'imageUrl'; 48 | const String dataArg = 'data'; 49 | const String notificationSpecificsArg = 'notificationSpecifics'; 50 | 51 | /// Util for displaying notifications for Android and iOS. 52 | class Notificator { 53 | static const _channel = MethodChannel(channelName); 54 | 55 | /// Callback notification clicks. 56 | final OnNotificationTapCallback onNotificationTapCallback; 57 | 58 | /// Callback notification decline(iOS only). 59 | final OnPermissionDeclineCallback? onPermissionDecline; 60 | 61 | @visibleForTesting 62 | final PlatformWrapper platform; 63 | 64 | IOSNotification? iosNotification; 65 | AndroidNotification? androidNotification; 66 | 67 | Notificator({ 68 | required this.onNotificationTapCallback, 69 | this.onPermissionDecline, 70 | this.iosNotification, 71 | this.androidNotification, 72 | PlatformWrapper? platform, 73 | MethodChannel? channel, 74 | }) : platform = platform ?? PlatformWrapper() { 75 | init(methodChannel: channel); 76 | } 77 | 78 | /// Request notification permissions (iOS only). 79 | Future requestPermissions({ 80 | bool? requestSoundPermission, 81 | bool? requestAlertPermission, 82 | }) { 83 | if (!platform.isIOS) { 84 | return Future.value(true); 85 | } else { 86 | return iosNotification!.requestPermissions( 87 | requestSoundPermission: requestSoundPermission, 88 | requestAlertPermission: requestAlertPermission, 89 | ); 90 | } 91 | } 92 | 93 | /// Show notification. 94 | /// 95 | /// [id] - notification identifier. 96 | /// [title] - title. 97 | /// [body] - the main text of the notification. 98 | /// [data] - data for notification. 99 | /// [notificationSpecifics] - notification specifics. 100 | Future show( 101 | int id, 102 | String title, 103 | String body, { 104 | String? imageUrl, 105 | Map? data, 106 | NotificationSpecifics? notificationSpecifics, 107 | }) { 108 | if (platform.isAndroid) { 109 | return androidNotification!.show( 110 | id, 111 | title, 112 | body, 113 | imageUrl, 114 | data, 115 | notificationSpecifics?.androidNotificationSpecifics, 116 | ); 117 | } else if (platform.isIOS) { 118 | return iosNotification!.show( 119 | id, 120 | title, 121 | body, 122 | imageUrl, 123 | data, 124 | notificationSpecifics?.iosNotificationSpecifics, 125 | ); 126 | } 127 | 128 | return Future.value(); 129 | } 130 | 131 | @visibleForTesting 132 | Future init({MethodChannel? methodChannel}) async { 133 | if (platform.isAndroid) { 134 | androidNotification ??= AndroidNotification( 135 | channel: methodChannel ?? _channel, 136 | onNotificationTap: onNotificationTapCallback, 137 | ); 138 | 139 | return androidNotification!.init(); 140 | } else if (platform.isIOS) { 141 | iosNotification ??= IOSNotification( 142 | channel: methodChannel ?? _channel, 143 | onNotificationTap: onNotificationTapCallback, 144 | onPermissionDecline: onPermissionDecline, 145 | ); 146 | 147 | return iosNotification!.init(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /test/notification_controller_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter_test/flutter_test.dart'; 16 | import 'package:mocktail/mocktail.dart'; 17 | import 'package:push_notification/push_notification.dart'; 18 | 19 | const _testTitle = 'Test title'; 20 | const _testBody = 'Test body'; 21 | const _testImageUrl = 'Test imageUrl'; 22 | const _testColor = 'Test color'; 23 | const _testIcon = 'Test icon'; 24 | const _id = 0; 25 | 26 | void main() { 27 | late NotificationController notificationController; 28 | late MockNotificator notificator; 29 | late MockPushHandleStrategy strategy; 30 | late MockNotificationPayload payload; 31 | late MockNotificationCallback notificationCallback; 32 | 33 | setUp(() { 34 | notificationCallback = MockNotificationCallback(); 35 | when(() => notificationCallback.call(any())) 36 | .thenAnswer((invocation) => Future.value()); 37 | 38 | notificator = MockNotificator(); 39 | when(() => notificator.requestPermissions( 40 | requestSoundPermission: any(named: 'requestSoundPermission'), 41 | requestAlertPermission: any(named: 'requestAlertPermission'), 42 | )).thenAnswer((_) => Future.value(true)); 43 | when(() => notificator.show( 44 | _id, 45 | _testTitle, 46 | _testBody, 47 | imageUrl: _testImageUrl, 48 | data: any(named: 'data'), 49 | notificationSpecifics: any(named: 'notificationSpecifics'), 50 | )).thenAnswer((invocation) => Future.value()); 51 | 52 | notificationController = NotificationController( 53 | () {}, 54 | transmittedNotificator: notificator, 55 | ); 56 | 57 | payload = MockNotificationPayload( 58 | messageData: {'test': 'test'}, 59 | title: _testTitle, 60 | body: _testBody, 61 | imageUrl: _testImageUrl, 62 | ); 63 | 64 | strategy = MockPushHandleStrategy( 65 | payload: payload, 66 | pushId: _id, 67 | notificationChannelId: 'Test notification channel id', 68 | notificationChannelName: 'Test notification channel name', 69 | autoCancelable: true, 70 | color: _testColor, 71 | icon: _testIcon, 72 | ); 73 | }); 74 | 75 | test( 76 | 'RequestPermissions methods should call correctly method at notificator', 77 | () async { 78 | await notificationController.requestPermissions( 79 | requestSoundPermission: true, 80 | requestAlertPermission: false, 81 | ); 82 | 83 | final args = verify( 84 | () => notificator.requestPermissions( 85 | requestSoundPermission: captureAny( 86 | named: 'requestSoundPermission', 87 | ), 88 | requestAlertPermission: captureAny( 89 | named: 'requestAlertPermission', 90 | ), 91 | ), 92 | ).captured; 93 | 94 | expect(args, equals([true, false])); 95 | }, 96 | ); 97 | 98 | test( 99 | 'Call show methods should call method show at notificator with correctly parameters', 100 | () { 101 | notificationController.show(strategy, (payload) {}); 102 | 103 | // The parameters must be the same as in the strategy. 104 | verify(() => notificator.show( 105 | _id, 106 | _testTitle, 107 | _testBody, 108 | imageUrl: _testImageUrl, 109 | data: any(named: 'data'), 110 | notificationSpecifics: any(named: 'notificationSpecifics'), 111 | )).called(1); 112 | }, 113 | ); 114 | 115 | test( 116 | 'If you do not pass the Notificator when creating a NotificationController, ' 117 | 'it will have a default value', 118 | () { 119 | notificationController = NotificationController(() {}); 120 | 121 | expect(notificationController.notificator, isNotNull); 122 | }, 123 | ); 124 | 125 | test( 126 | 'When call notificationController.internalOnSelectNotification method ' 127 | 'notigicationCallback should called', 128 | () { 129 | notificationController.callbackMap.addAll( 130 | {1: notificationCallback}, 131 | ); 132 | 133 | notificationController.internalOnSelectNotification( 134 | {'localPushId': '1'}, 135 | ); 136 | 137 | verify(() => notificationCallback({'localPushId': '1'})) 138 | .called(1); 139 | expect(notificationController.callbackMap, isEmpty); 140 | }, 141 | ); 142 | } 143 | 144 | class MockNotificator extends Mock implements Notificator {} 145 | 146 | class MockNotificationPayload extends Mock implements NotificationPayload { 147 | @override 148 | final Map messageData; 149 | @override 150 | final String title; 151 | @override 152 | final String body; 153 | @override 154 | final String? imageUrl; 155 | 156 | MockNotificationPayload({ 157 | required this.messageData, 158 | required this.title, 159 | required this.body, 160 | required this.imageUrl, 161 | }); 162 | } 163 | 164 | class MockPushHandleStrategy extends Mock implements PushHandleStrategy { 165 | @override 166 | final NotificationPayload payload; 167 | @override 168 | final int pushId; 169 | @override 170 | final String notificationChannelId; 171 | @override 172 | final String notificationChannelName; 173 | @override 174 | final bool autoCancelable; 175 | @override 176 | final String color; 177 | @override 178 | final String icon; 179 | 180 | MockPushHandleStrategy({ 181 | required this.payload, 182 | required this.pushId, 183 | required this.notificationChannelId, 184 | required this.notificationChannelName, 185 | required this.autoCancelable, 186 | required this.color, 187 | required this.icon, 188 | }); 189 | } 190 | 191 | class MockNotificationCallback extends Mock { 192 | void call(Map notificationData); 193 | } 194 | -------------------------------------------------------------------------------- /test/ios_notifiation_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/services.dart'; 16 | import 'package:flutter_test/flutter_test.dart'; 17 | import 'package:mocktail/mocktail.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | import 'package:push_notification/src/notification/notificator/ios/ios_notification.dart'; 20 | import 'package:push_notification/src/notification/notificator/ios/ios_notification_specifics.dart'; 21 | 22 | void main() { 23 | late IOSNotification iosNotification; 24 | late MockMethodChannel channel; 25 | late MockOnNotificationTapFunction onNotificationTapMethod; 26 | late MockOnPermissionDeclineFunction onPermissionDeclineMethod; 27 | 28 | setUp( 29 | () { 30 | channel = MockMethodChannel(); 31 | when(() => channel.invokeMethod( 32 | any(), 33 | any(), 34 | )).thenAnswer((invocation) => Future.value()); 35 | when(() => channel.setMethodCallHandler(any())) 36 | .thenAnswer((invocation) => Future.value()); 37 | 38 | onNotificationTapMethod = MockOnNotificationTapFunction(); 39 | when(() => onNotificationTapMethod.call(any())) 40 | .thenAnswer((invocation) => Future.value()); 41 | 42 | onPermissionDeclineMethod = MockOnPermissionDeclineFunction(); 43 | when(() => onPermissionDeclineMethod.call()) 44 | .thenAnswer((invocation) => Future.value()); 45 | 46 | iosNotification = IOSNotification( 47 | channel: channel, 48 | onNotificationTap: onNotificationTapMethod, 49 | onPermissionDecline: onPermissionDeclineMethod, 50 | ); 51 | }, 52 | ); 53 | 54 | test( 55 | 'When calling the init method, the channel.setMethodCallHandler method must ' 56 | 'be called', 57 | () async { 58 | await iosNotification.init(); 59 | 60 | verify(() => channel.setMethodCallHandler( 61 | iosNotification.methodCallHandlerCallback, 62 | )).called(1); 63 | }, 64 | ); 65 | 66 | group( 67 | 'Call methodCallHandlerCallback method: ', 68 | () { 69 | test( 70 | 'with openCallback MethodCall, the onNotificationTapMethod method ' 71 | 'must be called once with correctly parameter', 72 | () async { 73 | const methodCall = 74 | MethodCall(openCallback, {'Test arguments': 'Test'}); 75 | 76 | await iosNotification.methodCallHandlerCallback(methodCall); 77 | 78 | verify(() => onNotificationTapMethod(methodCall.arguments as Map)) 79 | .called(1); 80 | }, 81 | ); 82 | 83 | test( 84 | 'with permissionDeclineCallback MethodCall, the onPermissionDecline method' 85 | ' must be called once', 86 | () async { 87 | const methodCall = MethodCall( 88 | permissionDeclineCallback, 89 | {'Test arguments': 'Test'}, 90 | ); 91 | 92 | await iosNotification.methodCallHandlerCallback(methodCall); 93 | 94 | verify(() => onPermissionDeclineMethod()).called(1); 95 | }, 96 | ); 97 | 98 | test( 99 | 'with permissionDeclineCallback MethodCall, but without onPermissionDecline' 100 | ' in IOSNotification, onPermissionDecline method should not be called', 101 | () async { 102 | const methodCall = MethodCall( 103 | permissionDeclineCallback, 104 | {'Test arguments': 'Test'}, 105 | ); 106 | iosNotification = IOSNotification( 107 | channel: channel, 108 | onNotificationTap: onNotificationTapMethod, 109 | ); 110 | 111 | await iosNotification.methodCallHandlerCallback(methodCall); 112 | 113 | verifyNever(() => onPermissionDeclineMethod()); 114 | }, 115 | ); 116 | }, 117 | ); 118 | 119 | test( 120 | 'When calling the requestPermissions method, the channel.invokeMethod must ' 121 | 'be called with correctly parameters', 122 | () async { 123 | when( 124 | () => channel.invokeMethod( 125 | any(), 126 | any(), 127 | ), 128 | ).thenAnswer( 129 | (invocation) => Future.value(), 130 | ); 131 | 132 | await iosNotification.requestPermissions( 133 | requestAlertPermission: true, 134 | requestSoundPermission: true, 135 | ); 136 | 137 | verify(() => channel.invokeMethod( 138 | callRequest, 139 | { 140 | 'requestAlertPermission': true, 141 | 'requestSoundPermission': true, 142 | }, 143 | )).called(1); 144 | }, 145 | ); 146 | 147 | test( 148 | 'When calling the show method, the channel.invokeMethod must ' 149 | 'be called with correctly parameters', 150 | () async { 151 | const id = 1; 152 | const title = 'Test title'; 153 | const body = 'Test body'; 154 | const imageUrl = 'Test url'; 155 | const data = {'Test': 'Test strinf'}; 156 | 157 | await iosNotification.show( 158 | id, 159 | title, 160 | body, 161 | imageUrl, 162 | data, 163 | IosNotificationSpecifics(), 164 | ); 165 | 166 | verify(() => channel.invokeMethod( 167 | callShow, 168 | { 169 | pushIdArg: id, 170 | titleArg: title, 171 | bodyArg: body, 172 | imageUrlArg: imageUrl, 173 | dataArg: data, 174 | }, 175 | )).called(1); 176 | }, 177 | ); 178 | } 179 | 180 | class MockMethodChannel extends Mock implements MethodChannel {} 181 | 182 | class MockOnNotificationTapFunction extends Mock { 183 | void call(Map? notificationData); 184 | } 185 | 186 | class MockOnPermissionDeclineFunction extends Mock { 187 | void call(); 188 | } 189 | -------------------------------------------------------------------------------- /android/src/main/kotlin/pushnotification/push_notification/PushNotificationPlugin.kt: -------------------------------------------------------------------------------- 1 | package pushnotification.push_notification 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.AsyncTask 6 | import io.flutter.embedding.engine.plugins.FlutterPlugin 7 | import io.flutter.plugin.common.MethodCall 8 | import io.flutter.plugin.common.MethodChannel 9 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 10 | import io.flutter.plugin.common.MethodChannel.Result 11 | import pushnotification.push_notification.handler.PushHandler 12 | import pushnotification.push_notification.strategy.PushStrategy 13 | import pushnotification.push_notification.type.PushNotificationTypeData 14 | import ru.surfstudio.android.activity.holder.ActiveActivityHolder 15 | import ru.surfstudio.android.notification.interactor.push.PushInteractor 16 | import ru.surfstudio.android.notification.ui.PushClickProvider 17 | import ru.surfstudio.android.notification.ui.PushEventListener 18 | import ru.surfstudio.android.notification.ui.notification.NOTIFICATION_DATA 19 | 20 | //channels and methods names 21 | private const val CHANNEL = "surf_notification" 22 | private const val CALL_SHOW = "show" 23 | private const val CALL_INIT = "initialize" 24 | private const val CALLBACK_OPEN = "notificationOpen" 25 | private const val CALLBACK_DISMISS = "notificationDismiss" 26 | 27 | //resource folder names 28 | private const val FOLDER_DRAWABLE = "drawable" 29 | private const val FOLDER_STRING = "string" 30 | private const val FOLDER_COLOR = "color" 31 | 32 | ///arguments names 33 | private const val ARG_SPECIFICS = "notificationSpecifics" 34 | private const val ARG_ICON = "icon" 35 | private const val ARG_CHANNEL_ID = "channelId" 36 | private const val ARG_CHANNEL_NAME = "channelName" 37 | private const val ARG_COLOR = "color" 38 | private const val ARG_DATA = "data" 39 | private const val ARG_PUSH_ID = "pushId" 40 | private const val ARG_TITLE = "title" 41 | private const val ARG_BODY = "body" 42 | private const val ARG_IMAGE_URL = "imageUrl" 43 | private const val ARG_AUTOCANCELABLE = "autoCancelable" 44 | 45 | // default notification specifics 46 | private const val DEFAULT_ICON_NAME = "@mipmap/ic_launcher" 47 | private const val DEFAULT_CHANNEL_ID = "@string/notification_channel_id" 48 | private const val DEFAULT_CHANNEL_NAME = "@string/notification_channel_name" 49 | private const val DEFAULT_COLOR = "@color/design_default_color_primary" 50 | private const val DEFAULT_AUTOCANCEL = true 51 | 52 | /** SurfNotificationPlugin */ 53 | class PushNotificationPlugin(private var context: Context? = null, 54 | private var channel: MethodChannel? = null 55 | ) : FlutterPlugin, MethodCallHandler { 56 | 57 | private val activeActivityHolder = ActiveActivityHolder() 58 | private val pusInteractor = PushInteractor() 59 | 60 | private val pushHandler = PushHandler(activeActivityHolder, pusInteractor) 61 | 62 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 63 | val channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL) 64 | channel.setMethodCallHandler(PushNotificationPlugin(flutterPluginBinding.applicationContext, channel)) 65 | } 66 | 67 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 68 | context = null 69 | channel?.setMethodCallHandler(null) 70 | channel = null 71 | } 72 | 73 | override fun onMethodCall(call: MethodCall, result: Result) { 74 | val args = call.arguments as Map? 75 | 76 | when (call.method) { 77 | CALL_INIT -> { 78 | initNotificationTapListener() 79 | } 80 | CALL_SHOW -> handleNotification(args!!) 81 | else -> result.notImplemented() 82 | } 83 | } 84 | 85 | private fun initNotificationTapListener() { 86 | PushClickProvider.pushEventListener = object : PushEventListener { 87 | override fun customActionListener(context: Context, intent: Intent) { 88 | } 89 | 90 | override fun pushDismissListener(context: Context, intent: Intent) { 91 | channel!!.invokeMethod(CALLBACK_DISMISS, null) 92 | } 93 | 94 | override fun pushOpenListener(context: Context, intent: Intent) { 95 | val notificationTypeData = intent.getSerializableExtra(NOTIFICATION_DATA) as PushNotificationTypeData 96 | val notificationData = HashMap(notificationTypeData.data?.notificationData) 97 | channel!!.invokeMethod(CALLBACK_OPEN, notificationData) 98 | } 99 | } 100 | } 101 | 102 | private fun handleNotification(args: Map) { 103 | val notificationSpecifics = args[ARG_SPECIFICS] as HashMap 104 | val icon = notificationSpecifics[ARG_ICON] as String? ?: DEFAULT_ICON_NAME 105 | val channelId = notificationSpecifics[ARG_CHANNEL_ID] as String? ?: DEFAULT_CHANNEL_ID 106 | val channelName = notificationSpecifics[ARG_CHANNEL_NAME] as String? ?: DEFAULT_CHANNEL_NAME 107 | val color = notificationSpecifics[ARG_COLOR] as String? ?: DEFAULT_COLOR 108 | val imageUrl = args[ARG_IMAGE_URL] as String? 109 | val autoCancelable: Boolean = notificationSpecifics[ARG_AUTOCANCELABLE] as Boolean? 110 | ?: DEFAULT_AUTOCANCEL 111 | 112 | val data = args[ARG_DATA] as HashMap? 113 | 114 | val strategy = PushStrategy( 115 | icon = getResourceId(icon, FOLDER_DRAWABLE), 116 | channelId = getResourceId(channelId, FOLDER_STRING), 117 | channelName = getResourceId(channelName, FOLDER_STRING), 118 | color = getResourceId(color, FOLDER_COLOR), 119 | autoCancelable = autoCancelable, 120 | imageUrl = imageUrl) 121 | 122 | if (data != null) { 123 | strategy.typeData.setDataFromMap(data) 124 | } 125 | 126 | AsyncTask.execute { 127 | pushHandler.handleMessage(context!!, 128 | uniqueId = args[ARG_PUSH_ID] as Int? ?: -1, 129 | title = args[ARG_TITLE] as String? ?: "", 130 | body = args[ARG_BODY] as String? ?: "", 131 | pushHandleStrategy = strategy 132 | ) 133 | } 134 | } 135 | 136 | private fun getResourceId(resName: String, defType: String): Int { 137 | return context!!.resources.getIdentifier(resName, defType, context!!.packageName) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /test/push_handler_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter_test/flutter_test.dart'; 16 | import 'package:mocktail/mocktail.dart'; 17 | import 'package:push_notification/src/base/base_messaging_service.dart'; 18 | import 'package:push_notification/src/base/push_handle_strategy.dart'; 19 | import 'package:push_notification/src/base/push_handle_strategy_factory.dart'; 20 | import 'package:push_notification/src/notification/notification_controller.dart'; 21 | import 'package:push_notification/src/push_handler.dart'; 22 | import 'package:push_notification/src/util/platform_wrapper.dart'; 23 | 24 | void main() { 25 | setUpAll(() { 26 | registerFallbackValue(MockPushHandleStrategy()); 27 | }); 28 | 29 | late PushHandler handler; 30 | late MockNotificationController notificationController; 31 | late MockPushHandleStrategy pushHandleStrategy; 32 | late MockPlatformWrapper platform; 33 | late MockPushHandleStrategyFactory pushHandleStrategyFactory; 34 | 35 | setUp(() { 36 | pushHandleStrategy = MockPushHandleStrategy(); 37 | 38 | notificationController = MockNotificationController(); 39 | when(() => notificationController.requestPermissions( 40 | requestSoundPermission: any(named: 'requestSoundPermission'), 41 | requestAlertPermission: any(named: 'requestAlertPermission'), 42 | )).thenAnswer((_) => Future.value(true)); 43 | when(() => notificationController.show(any(), any())) 44 | .thenAnswer((invocation) async { 45 | return (invocation.positionalArguments[1] as NotificationCallback) 46 | .call({}); 47 | }); 48 | 49 | platform = MockPlatformWrapper(); 50 | pushHandleStrategyFactory = MockPushHandleStrategyFactory(); 51 | when(() => pushHandleStrategyFactory.createByData(any())) 52 | .thenReturn(pushHandleStrategy); 53 | 54 | handler = PushHandler( 55 | pushHandleStrategyFactory, 56 | notificationController, 57 | MockBaseMessagingService(), 58 | platform: platform, 59 | ); 60 | }); 61 | 62 | group( 63 | 'Call requestPermissions method:', 64 | () { 65 | test( 66 | 'if the platform is iOS requestPermissions methods should call correctly' 67 | ' method at notificashionController', 68 | () { 69 | when(() => platform.isAndroid).thenReturn(false); 70 | when(() => platform.isIOS).thenReturn(true); 71 | 72 | handler.requestPermissions( 73 | soundPemission: false, 74 | alertPermission: true, 75 | ); 76 | 77 | final args = verify( 78 | () => notificationController.requestPermissions( 79 | requestSoundPermission: 80 | captureAny(named: 'requestSoundPermission'), 81 | requestAlertPermission: 82 | captureAny(named: 'requestAlertPermission'), 83 | ), 84 | ).captured; 85 | 86 | expect(args, equals([false, true])); 87 | }, 88 | ); 89 | 90 | test( 91 | 'if platfotm is not iOS requestPermissions method should return null', 92 | () async { 93 | when(() => platform.isAndroid).thenReturn(true); 94 | when(() => platform.isIOS).thenReturn(false); 95 | 96 | final response = await handler.requestPermissions( 97 | soundPemission: false, 98 | alertPermission: true, 99 | ); 100 | 101 | expect(response, null); 102 | }, 103 | ); 104 | }, 105 | ); 106 | 107 | group('Call handleMessage method:', () { 108 | test( 109 | 'if MessageHandlerType is onBackgroundMessage messageSubject should receive correctly message', 110 | () async { 111 | const message = {'message': 'simple on launch text'}; 112 | final messages = >[]; 113 | 114 | handler.messageSubject.listen(messages.add); 115 | 116 | expect(messages, isEmpty); 117 | 118 | handler.handleMessage(message, MessageHandlerType.onBackgroundMessage); 119 | await handler.messageSubject.close(); 120 | 121 | expect(messages, equals([message])); 122 | verify(() => pushHandleStrategy.onBackgroundProcess(message)) 123 | .called(equals(1)); 124 | verifyNever(() => notificationController.show(any(), any())); 125 | }, 126 | ); 127 | 128 | test( 129 | 'if MessageHandlerType is onMessage messageSubject should receive correctly message', 130 | () async { 131 | const message = {'message': 'simple on message text'}; 132 | final messages = >[]; 133 | 134 | handler.messageSubject.listen(messages.add); 135 | handler.handleMessage( 136 | message, 137 | MessageHandlerType.onMessage, 138 | ); 139 | await handler.messageSubject.close(); 140 | 141 | expect(messages, equals([message])); 142 | verifyNever(() => pushHandleStrategy.onBackgroundProcess(any())); 143 | verify(() => notificationController.show(any(), any())) 144 | .called(equals(1)); 145 | }, 146 | ); 147 | 148 | test( 149 | 'if MessageHandlerType is onMessageOpenedApp messageSubject shouldn not receive a message', 150 | () async { 151 | const message = {'message': 'simple on resume text'}; 152 | final messages = >[]; 153 | 154 | handler.messageSubject.listen(messages.add); 155 | handler.handleMessage( 156 | message, 157 | MessageHandlerType.onMessageOpenedApp, 158 | localNotification: true, 159 | ); 160 | await handler.messageSubject.close(); 161 | 162 | expect(messages, isEmpty); 163 | verifyNever(() => pushHandleStrategy.onBackgroundProcess(message)); 164 | verifyNever(() => notificationController.show(any(), any())); 165 | }, 166 | ); 167 | }); 168 | 169 | test( 170 | 'If platform is not passed to the PushHandler, platform should be ' 171 | 'initialized with a default value', 172 | () { 173 | final testPlatformHandler = PushHandler( 174 | pushHandleStrategyFactory, 175 | notificationController, 176 | MockBaseMessagingService(), 177 | ); 178 | 179 | expect(testPlatformHandler.platform, isNotNull); 180 | }, 181 | ); 182 | } 183 | 184 | class MockBaseMessagingService extends Mock implements BaseMessagingService {} 185 | 186 | class MockPushHandleStrategyFactory extends Mock 187 | implements PushHandleStrategyFactory {} 188 | 189 | class MockPushHandleStrategy extends Mock implements PushHandleStrategy {} 190 | 191 | class MockNotificationController extends Mock 192 | implements NotificationController {} 193 | 194 | class MockPlatformWrapper extends Mock implements PlatformWrapper {} 195 | -------------------------------------------------------------------------------- /test/push_handle_strategy_factory_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter_test/flutter_test.dart'; 16 | import 'package:mocktail/mocktail.dart'; 17 | import 'package:push_notification/push_notification.dart'; 18 | import 'package:push_notification/src/util/platform_wrapper.dart'; 19 | 20 | void main() { 21 | late TestPushHandleStrategyFactoryWithoutMap strategyFactoryWithoutMap; 22 | late TestPushHandleStrategyFactoryWithMap strategyFactoryWithMap; 23 | late MockPlatformWrapper platformWrapper; 24 | late MockPushHandleStrategy mockPushHandleStrategy; 25 | late MockDefaultPushHandleStrategy defaultPushHandleStrategy; 26 | 27 | setUp( 28 | () { 29 | platformWrapper = MockPlatformWrapper(); 30 | mockPushHandleStrategy = MockPushHandleStrategy(); 31 | defaultPushHandleStrategy = MockDefaultPushHandleStrategy(); 32 | strategyFactoryWithoutMap = TestPushHandleStrategyFactoryWithoutMap( 33 | platformWrapper: platformWrapper, 34 | strategy: defaultPushHandleStrategy, 35 | ); 36 | strategyFactoryWithMap = TestPushHandleStrategyFactoryWithMap( 37 | platformWrapper: platformWrapper, 38 | defStrategy: defaultPushHandleStrategy, 39 | strategy: mockPushHandleStrategy, 40 | ); 41 | }, 42 | ); 43 | 44 | test( 45 | 'If platform is not passed to the TestPushHandleStrategyFactory, platform should be ' 46 | 'initialized with a default value', 47 | () { 48 | final testPlatformPushHandleStrategyFactory = 49 | TestPushHandleStrategyFactoryWithMap( 50 | defStrategy: defaultPushHandleStrategy, 51 | strategy: mockPushHandleStrategy, 52 | ); 53 | 54 | expect( 55 | testPlatformPushHandleStrategyFactory.platform, 56 | isNotNull, 57 | ); 58 | }, 59 | ); 60 | 61 | test( 62 | 'If you do not override the map method, it should return an empty value', 63 | () { 64 | final ecpectedResponse = {}; 65 | 66 | final response = strategyFactoryWithoutMap.map; 67 | 68 | expect(response, ecpectedResponse); 69 | }, 70 | ); 71 | 72 | group( 73 | 'Call createByData method on Android ', 74 | () { 75 | test( 76 | 'with correct key and value in messageData should return correctly PushHandleStrategy', 77 | () { 78 | final messageData = { 79 | 'data': {'event': 'Test message'}, 80 | }; 81 | when(() => platformWrapper.isAndroid).thenReturn(true); 82 | when(() => platformWrapper.isIOS).thenReturn(false); 83 | 84 | final response = strategyFactoryWithMap.createByData(messageData); 85 | 86 | expect(response, mockPushHandleStrategy); 87 | }, 88 | ); 89 | 90 | test( 91 | 'with not correct key in messageData should return default PushHandleStrategy', 92 | () { 93 | final messageData = { 94 | 'data': {'not correct event': 'Test message'}, 95 | }; 96 | when(() => platformWrapper.isAndroid).thenReturn(true); 97 | when(() => platformWrapper.isIOS).thenReturn(false); 98 | 99 | final response = strategyFactoryWithMap.createByData(messageData); 100 | 101 | expect(response, defaultPushHandleStrategy); 102 | }, 103 | ); 104 | 105 | test( 106 | 'with not correct value in messageData should return correctly PushHandleStrategy', 107 | () { 108 | final messageData = { 109 | 'data': {'event': 'Test message not correct'}, 110 | }; 111 | when(() => platformWrapper.isAndroid).thenReturn(true); 112 | when(() => platformWrapper.isIOS).thenReturn(false); 113 | 114 | final response = strategyFactoryWithMap.createByData(messageData); 115 | 116 | expect(response, defaultPushHandleStrategy); 117 | }, 118 | ); 119 | }, 120 | ); 121 | 122 | group( 123 | 'Call createByData method on iOS ', 124 | () { 125 | test( 126 | 'with correct key and value in messageData should return correctly PushHandleStrategy', 127 | () { 128 | final messageData = { 129 | 'data': {'event': 'Test message'} 130 | }; 131 | when(() => platformWrapper.isAndroid).thenReturn(false); 132 | when(() => platformWrapper.isIOS).thenReturn(true); 133 | 134 | final response = strategyFactoryWithMap.createByData(messageData); 135 | 136 | expect(response, mockPushHandleStrategy); 137 | }, 138 | ); 139 | 140 | test( 141 | 'with not correct key in messageData should return default PushHandleStrategy', 142 | () { 143 | final messageData = { 144 | 'data': {'Not correct event': 'Test message'} 145 | }; 146 | when(() => platformWrapper.isAndroid).thenReturn(false); 147 | when(() => platformWrapper.isIOS).thenReturn(true); 148 | 149 | final response = strategyFactoryWithMap.createByData(messageData); 150 | 151 | expect(response, defaultPushHandleStrategy); 152 | }, 153 | ); 154 | 155 | test( 156 | 'with not correct value in messageData should return correctly PushHandleStrategy', 157 | () { 158 | final messageData = { 159 | 'data': {'event': 'Test message nor correct'} 160 | }; 161 | when(() => platformWrapper.isAndroid).thenReturn(false); 162 | when(() => platformWrapper.isIOS).thenReturn(true); 163 | 164 | final response = strategyFactoryWithMap.createByData(messageData); 165 | 166 | expect(response, defaultPushHandleStrategy); 167 | }, 168 | ); 169 | }, 170 | ); 171 | } 172 | 173 | /// Сlass with overridden map method for testing the createByData method. 174 | class TestPushHandleStrategyFactoryWithMap extends PushHandleStrategyFactory { 175 | final PushHandleStrategy defStrategy; 176 | final PushHandleStrategy strategy; 177 | 178 | @override 179 | StrategyBuilder get defaultStrategy { 180 | return (payload) => defStrategy; 181 | } 182 | 183 | @override 184 | Map get map => { 185 | 'Test message': (payload) { 186 | return strategy; 187 | }, 188 | }; 189 | 190 | TestPushHandleStrategyFactoryWithMap({ 191 | required this.defStrategy, 192 | required this.strategy, 193 | MockPlatformWrapper? platformWrapper, 194 | }) : super(platformWrapper: platformWrapper); 195 | } 196 | 197 | /// Сlass with non-overridden map method for testing a map method. 198 | class TestPushHandleStrategyFactoryWithoutMap 199 | extends PushHandleStrategyFactory { 200 | final PushHandleStrategy strategy; 201 | 202 | @override 203 | StrategyBuilder get defaultStrategy { 204 | return (payload) => strategy; 205 | } 206 | 207 | TestPushHandleStrategyFactoryWithoutMap({ 208 | required MockPlatformWrapper platformWrapper, 209 | required this.strategy, 210 | }) : super(platformWrapper: platformWrapper); 211 | } 212 | 213 | class MockDefaultPushHandleStrategy extends Mock 214 | implements PushHandleStrategy {} 215 | 216 | class MockPushHandleStrategy extends Mock implements PushHandleStrategy {} 217 | 218 | class MockPlatformWrapper extends Mock implements PlatformWrapper {} 219 | -------------------------------------------------------------------------------- /ios/Classes/SwiftPushNotificationPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import UserNotifications 4 | 5 | // Channels and methods names 6 | let CHANNEL = "surf_notification" 7 | let CALL_SHOW = "show" 8 | let CALL_REQUEST = "request" 9 | let CALLBACK_OPEN = "notificationOpen" 10 | let CALLBACK_PERMISSION_DECLINE = "permissionDecline" 11 | // Arguments names 12 | let ARG_PUSH_ID = "pushId" 13 | let ARG_TITLE = "title" 14 | let ARG_BODY = "body" 15 | let ARG_IMAGE_URL = "imageUrl" 16 | let ARG_DATA = "data" 17 | 18 | // Plugin to display notifications 19 | @available(iOS 10.0, *) 20 | public class SwiftPushNotificationPlugin: NSObject, FlutterPlugin, UNUserNotificationCenterDelegate { 21 | 22 | let notificationCenter = UNUserNotificationCenter.current() 23 | 24 | var channel :FlutterMethodChannel 25 | 26 | init(channel: FlutterMethodChannel) { 27 | self.channel = channel 28 | super.init() 29 | notificationCenter.delegate = self 30 | } 31 | 32 | public static func register(with registrar: FlutterPluginRegistrar) { 33 | let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: registrar.messenger()) 34 | let instance = SwiftPushNotificationPlugin(channel: channel) 35 | registrar.addMethodCallDelegate(instance, channel: channel) 36 | } 37 | 38 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 39 | let args = call.arguments as! NSDictionary 40 | switch call.method{ 41 | case CALL_REQUEST: 42 | requestPermissions(args: args) 43 | break 44 | case CALL_SHOW: 45 | show(args: args) 46 | break; 47 | default: 48 | result(FlutterMethodNotImplemented) 49 | return 50 | } 51 | } 52 | 53 | // Initialize Notifications 54 | func requestPermissions(args : NSDictionary) { 55 | // Read notification request request parameter 56 | let requestAlertPermission = args["requestAlertPermission"] as! Bool 57 | // Read request notification sound parameter 58 | let requestSoundPermission = args["requestSoundPermission"] as! Bool 59 | // Notification Options 60 | var options: UNAuthorizationOptions = [] 61 | 62 | // Enable notifications if requestAlertPermission == true 63 | if requestAlertPermission { 64 | options.insert(.alert) 65 | } 66 | // Enable notification sound if requestSoundPermission == true 67 | if requestSoundPermission { 68 | options.insert(.sound) 69 | } 70 | 71 | // Permission request for notifications 72 | notificationCenter.requestAuthorization(options: options) { 73 | (didAllow, error) in 74 | if !didAllow { 75 | self.channel.invokeMethod(CALLBACK_PERMISSION_DECLINE, arguments: nil) 76 | return 77 | } 78 | print("notification request is done") 79 | } 80 | } 81 | 82 | // Show notifications 83 | func show(args: NSDictionary) { 84 | // Notification id 85 | let id: Int = args[ARG_PUSH_ID] as! Int 86 | // Notification title 87 | let title: String = args[ARG_TITLE] as! String 88 | // Notification body text 89 | let body: String = args[ARG_BODY] as! String 90 | // Notification image url 91 | let imageUrl: String? = args[ARG_IMAGE_URL] as? String 92 | // Data for notification 93 | var data: Dictionary = [:] 94 | 95 | if !(args[ARG_DATA] is NSNull){ 96 | data = args[ARG_DATA] as! Dictionary 97 | } 98 | 99 | // The object in which previously received data is stored 100 | let content = UNMutableNotificationContent() 101 | 102 | content.title = title 103 | content.body = body 104 | content.sound = UNNotificationSound.default 105 | content.userInfo = data 106 | 107 | if let path = imageUrl { 108 | if let url = URL(string: path){ 109 | if let imageData = NSData(contentsOf: url){ 110 | if let attachment = UNNotificationAttachment.create(imageFileIdentifier: "image.jpg", data: imageData, options: nil) { 111 | content.attachments = [ attachment ] 112 | } else { 113 | print("error in UNNotificationAttachment.create()") 114 | } 115 | } 116 | } 117 | } 118 | 119 | 120 | /* Trigger when notification is displayed 121 |       Now it is configured to show 122 |       notification with a minimum delay without repetitions 123 | */ 124 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) 125 | let identifier = String(id) 126 | // Notification request 127 | let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) 128 | 129 | // Add notification to notificationCenter 130 | // After that, a notification is displayed 131 | notificationCenter.add(request) 132 | } 133 | 134 | /* Called when the application is in the foreground. We get a UNNotification object that contains the UNNotificationRequest request. In the body of the method, you need to make a completion handler call with a set of options to notify UNNotificationPresentationOptions 135 | */ 136 | @available(iOS 10.0, *) 137 | public func userNotificationCenter(_ center: UNUserNotificationCenter, 138 | willPresent notification: UNNotification, 139 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 140 | completionHandler([.alert,.sound]) 141 | } 142 | 143 | /* Used to select a tap action for notification. You get a UNNotificationResponse object that contains an actionIdentifier to define an action. The system identifiers UNNotificationDefaultActionIdentifier and UNNotificationDismissActionIdentifier are used when you need to open the application by tap on a notification or close a notification with a swipe. 144 | */ 145 | @available(iOS 10.0, *) 146 | public func userNotificationCenter(_ center: UNUserNotificationCenter, 147 | didReceive response: UNNotificationResponse, 148 | withCompletionHandler completionHandler: @escaping () -> Void) { 149 | let notification = response.notification 150 | let notificationData = notification.request.content.userInfo 151 | 152 | channel.invokeMethod(CALLBACK_OPEN, arguments: notificationData) 153 | completionHandler() 154 | } 155 | } 156 | 157 | @available(iOS 10.0, *) 158 | @available(iOSApplicationExtension 10.0, *) 159 | extension UNNotificationAttachment { 160 | 161 | /// Save the image to disk 162 | static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? { 163 | let fileManager = FileManager.default 164 | let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString 165 | guard let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true) else { return nil } 166 | 167 | do { 168 | try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil) 169 | let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier) 170 | try data.write(to: fileURL, options: []) 171 | let imageAttachment = try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL, options: options) 172 | return imageAttachment 173 | } catch let error { 174 | print("error \(error)") 175 | } 176 | 177 | return nil 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "{}" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 SurfStudio LLC 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /test/notificator_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-present, SurfStudio LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/services.dart'; 16 | import 'package:flutter_test/flutter_test.dart'; 17 | import 'package:mocktail/mocktail.dart'; 18 | import 'package:push_notification/push_notification.dart'; 19 | import 'package:push_notification/src/notification/notificator/android/android_notification.dart'; 20 | import 'package:push_notification/src/notification/notificator/ios/ios_notification.dart'; 21 | import 'package:push_notification/src/util/platform_wrapper.dart'; 22 | 23 | void main() { 24 | const id = 1; 25 | const title = 'Test title'; 26 | const body = 'Test body'; 27 | const imageUrl = 'Test url'; 28 | const data = {'Test key': 'Test value'}; 29 | 30 | late Notificator notificator; 31 | late MockPlatformWrapper platform; 32 | late MockiOSNotification iosNotification; 33 | late MockAndroidNotification androidNotification; 34 | late MockAndroidNotificationSpecifics androidNotificationSpecifics; 35 | late NotificationSpecifics notificationSpecifics; 36 | late MockMethodChannel methodChannel; 37 | 38 | setUp( 39 | () { 40 | platform = MockPlatformWrapper(); 41 | 42 | iosNotification = MockiOSNotification(); 43 | when(() => iosNotification.init()).thenAnswer( 44 | (_) => Future.value(), 45 | ); 46 | when( 47 | () => iosNotification.requestPermissions( 48 | requestSoundPermission: any(named: 'requestSoundPermission'), 49 | requestAlertPermission: any(named: 'requestAlertPermission'), 50 | ), 51 | ).thenAnswer((_) => Future.value(true)); 52 | when(() => iosNotification.show( 53 | any(), 54 | any(), 55 | any(), 56 | any(), 57 | any(), 58 | any(), 59 | )).thenAnswer((invocation) => Future.value()); 60 | 61 | androidNotification = MockAndroidNotification(); 62 | when(() => androidNotification.init()).thenAnswer( 63 | (_) => Future.value(), 64 | ); 65 | when(() => androidNotification.show( 66 | any(), 67 | any(), 68 | any(), 69 | any(), 70 | any(), 71 | any(), 72 | )).thenAnswer((invocation) => Future.value()); 73 | 74 | androidNotificationSpecifics = MockAndroidNotificationSpecifics(); 75 | 76 | notificationSpecifics = 77 | NotificationSpecifics(androidNotificationSpecifics); 78 | 79 | methodChannel = MockMethodChannel(); 80 | when(() => methodChannel.invokeMethod(any())) 81 | .thenAnswer((invocation) => Future.value()); 82 | }, 83 | ); 84 | 85 | test( 86 | 'If platform is not passed to the Notificator, platform should be ' 87 | 'initialized with a default value', 88 | () { 89 | when(() => platform.isAndroid).thenReturn(false); 90 | when(() => platform.isIOS).thenReturn(false); 91 | 92 | notificator = Notificator( 93 | onNotificationTapCallback: (notificationData) {}, 94 | onPermissionDecline: () {}, 95 | iosNotification: iosNotification, 96 | ); 97 | 98 | expect(notificator.platform, isNotNull); 99 | }, 100 | ); 101 | 102 | test( 103 | 'Method show should return Future.value() if platform not Android ' 104 | 'and not iOS', 105 | () { 106 | when(() => platform.isAndroid).thenReturn(false); 107 | when(() => platform.isIOS).thenReturn(false); 108 | 109 | notificator = Notificator( 110 | onNotificationTapCallback: (notificationData) {}, 111 | onPermissionDecline: () {}, 112 | iosNotification: iosNotification, 113 | platform: platform, 114 | ); 115 | final expectedResponse = Future.value(); 116 | 117 | final response = notificator.show( 118 | id, 119 | title, 120 | body, 121 | imageUrl: imageUrl, 122 | data: data, 123 | notificationSpecifics: notificationSpecifics, 124 | ); 125 | 126 | expect(response.runtimeType, expectedResponse.runtimeType); 127 | }, 128 | ); 129 | 130 | group( 131 | 'Сreating notificator and calling its methods on the platform iOS:', 132 | () { 133 | test( 134 | 'If you do not pass the iosNotification when creating a notificator, ' 135 | 'it will have a default value', 136 | () { 137 | when(() => platform.isAndroid).thenReturn(false); 138 | when(() => platform.isIOS).thenReturn(true); 139 | 140 | notificator = Notificator( 141 | onNotificationTapCallback: (notificationData) {}, 142 | onPermissionDecline: () {}, 143 | platform: platform, 144 | channel: methodChannel, 145 | ); 146 | 147 | expect(notificator.iosNotification, isNotNull); 148 | }, 149 | ); 150 | 151 | test( 152 | 'When creating notificator the init method must be called once', 153 | () { 154 | when(() => platform.isAndroid).thenReturn(false); 155 | when(() => platform.isIOS).thenReturn(true); 156 | 157 | notificator = Notificator( 158 | onNotificationTapCallback: (notificationData) {}, 159 | onPermissionDecline: () {}, 160 | iosNotification: iosNotification, 161 | platform: platform, 162 | ); 163 | 164 | verify(() => iosNotification.init()).called(1); 165 | 166 | expect(() => notificator.init(), returnsNormally); 167 | }, 168 | ); 169 | 170 | test( 171 | 'Method requestPermissions should work correctly', 172 | () { 173 | when(() => platform.isAndroid).thenReturn(false); 174 | when(() => platform.isIOS).thenReturn(true); 175 | 176 | notificator = Notificator( 177 | onNotificationTapCallback: (notificationData) {}, 178 | onPermissionDecline: () {}, 179 | iosNotification: iosNotification, 180 | platform: platform, 181 | )..requestPermissions( 182 | requestSoundPermission: false, 183 | requestAlertPermission: true, 184 | ); 185 | 186 | final args = verify( 187 | () => iosNotification.requestPermissions( 188 | requestSoundPermission: 189 | captureAny(named: 'requestSoundPermission'), 190 | requestAlertPermission: 191 | captureAny(named: 'requestAlertPermission'), 192 | ), 193 | ).captured; 194 | 195 | expect(args, equals([false, true])); 196 | }, 197 | ); 198 | 199 | test( 200 | 'Method call requestPermissions not for iOS should return true', 201 | () async { 202 | when(() => platform.isAndroid).thenReturn(true); 203 | when(() => platform.isIOS).thenReturn(false); 204 | 205 | notificator = Notificator( 206 | onNotificationTapCallback: (notificationData) {}, 207 | onPermissionDecline: () {}, 208 | platform: platform, 209 | androidNotification: androidNotification, 210 | channel: methodChannel, 211 | ); 212 | 213 | final response = await notificator.requestPermissions( 214 | requestSoundPermission: false, 215 | requestAlertPermission: true, 216 | ); 217 | 218 | expect(response, true); 219 | }, 220 | ); 221 | 222 | test( 223 | 'Method show should work correctly', 224 | () { 225 | when(() => platform.isAndroid).thenReturn(false); 226 | when(() => platform.isIOS).thenReturn(true); 227 | 228 | notificator = Notificator( 229 | onNotificationTapCallback: (notificationData) {}, 230 | onPermissionDecline: () {}, 231 | iosNotification: iosNotification, 232 | platform: platform, 233 | )..show( 234 | id, 235 | title, 236 | body, 237 | imageUrl: imageUrl, 238 | data: data, 239 | notificationSpecifics: notificationSpecifics, 240 | ); 241 | 242 | verify(() => iosNotification.show( 243 | id, 244 | title, 245 | body, 246 | imageUrl, 247 | data, 248 | any(), 249 | )).called(1); 250 | }, 251 | ); 252 | }, 253 | ); 254 | 255 | group( 256 | 'Сreating notificator and calling its methods on the platform Android:', 257 | () { 258 | test( 259 | 'If you do not pass the AndroidNotification when creating a notificator, ' 260 | 'it will have a default value', 261 | () { 262 | when(() => platform.isAndroid).thenReturn(true); 263 | when(() => platform.isIOS).thenReturn(false); 264 | 265 | notificator = Notificator( 266 | onNotificationTapCallback: (notificationData) {}, 267 | onPermissionDecline: () {}, 268 | platform: platform, 269 | channel: methodChannel, 270 | ); 271 | 272 | expect( 273 | notificator.androidNotification, 274 | isNotNull, 275 | ); 276 | }, 277 | ); 278 | 279 | test( 280 | 'When creating notificator the init method must be called once', 281 | () { 282 | when(() => platform.isAndroid).thenReturn(true); 283 | when(() => platform.isIOS).thenReturn(false); 284 | 285 | notificator = Notificator( 286 | onNotificationTapCallback: (notificationData) {}, 287 | onPermissionDecline: () {}, 288 | androidNotification: androidNotification, 289 | platform: platform, 290 | ); 291 | 292 | verify(() => androidNotification.init()).called(1); 293 | 294 | expect(() => notificator.init(), returnsNormally); 295 | }, 296 | ); 297 | 298 | test( 299 | 'Method show should work correctly', 300 | () { 301 | when(() => platform.isAndroid).thenReturn(true); 302 | when(() => platform.isIOS).thenReturn(false); 303 | 304 | notificator = Notificator( 305 | onNotificationTapCallback: (notificationData) {}, 306 | onPermissionDecline: () {}, 307 | androidNotification: androidNotification, 308 | platform: platform, 309 | )..show( 310 | id, 311 | title, 312 | body, 313 | imageUrl: imageUrl, 314 | data: data, 315 | notificationSpecifics: notificationSpecifics, 316 | ); 317 | 318 | verify(() => androidNotification.show( 319 | id, 320 | title, 321 | body, 322 | imageUrl, 323 | data, 324 | notificationSpecifics.androidNotificationSpecifics, 325 | )).called(1); 326 | }, 327 | ); 328 | }, 329 | ); 330 | } 331 | 332 | class MockPlatformWrapper extends Mock implements PlatformWrapper {} 333 | 334 | class MockiOSNotification extends Mock implements IOSNotification {} 335 | 336 | class MockAndroidNotification extends Mock implements AndroidNotification {} 337 | 338 | class MockMethodChannel extends Mock implements MethodChannel {} 339 | 340 | class MockAndroidNotificationSpecifics extends Mock 341 | implements AndroidNotificationSpecifics {} 342 | --------------------------------------------------------------------------------