diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3693dd64..5176e5e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,8 @@ name: CI on: [pull_request] env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer - FLUTTER_VERSION: 3.22.0 + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + FLUTTER_VERSION: 3.24.3 JAVA_VERSION: "17.x" JAVA_DISTRIBUTION: 'zulu' ANDROID_SDK_ROOT: ${{ github.workspace }}/android-sdk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c784d70..2b0d8be2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,8 @@ on: - "[0-9]+.[0-9]+.[0-9]+*" env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer - FLUTTER_VERSION: 3.22.0 + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + FLUTTER_VERSION: 3.24.3 JAVA_VERSION: "17.x" JAVA_DISTRIBUTION: 'zulu' diff --git a/CHANGELOG.md b/CHANGELOG.md index 62340563..3f931fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Flutter Plugin Changelog +## Version 7.9.0 - October 20, 2024 + +Minor version release with several new features including: iOS Live Activities, Android Live Updates, Message Center improvements, and iOS notification service extension support in the iOS example project. + +### Changes + +- Updated Airship Android SDK to [18.3.3](https://github.com/urbanairship/android-library/releases/tag/18.3.3) +- Updated Airship iOS SDK to [18.11.1](https://github.com/urbanairship/ios-library/releases/tag/18.11.1) +- Added `notificationPermissionStatus` to `PushNotificationStatus` +- Added options to `enableUserNotifications` to specify the `PromptPermissionFallback` when enabling user notifications +- Added new `showMessageCenter(messageId?: string)` and `showMessageView(messageId: string)` to `MessageCenter` to display the OOTB UI even if `autoLaunchDefaultMessageCenter` is disabled +- Added new APIs to manage [iOS Live Activities](https://docs.airship.com/platform/mobile/ios-live-activities/) +- Added new APIs to manage [Android Live Updates](https://docs.airship.com/platform/mobile/android-live-updates/) +- Added a new [iOS plugin extender]() to modify the native Airship SDK after takeOff +- Added new [Android plugin extender]() to modify the native Airship SDK after takeOff + +## Version 7.8.2 - September 13, 2024 + +Patch release that fixes a potential Swift compile error. + +### Changes + +- Fixed a potential Swift compile error. + ## Version 7.8.2 - September 13, 2024 Patch release that fixes a potential Swift compile error. diff --git a/analysis_options.yaml b/analysis_options.yaml index 12f7f5c3..9a8aeb35 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,4 +2,5 @@ include: package:lints/recommended.yaml linter: rules: - constant_identifier_names: false \ No newline at end of file + constant_identifier_names: false + prefer_const_constructors: false \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 20b79e13..49c6831e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - group 'com.airship.airship' version '1.0-SNAPSHOT' @@ -7,7 +5,7 @@ buildscript { ext.kotlin_version = '1.9.0' ext.coroutine_version = '1.5.2' ext.datastore_preferences_version = '1.1.1' - ext.airship_framework_proxy_version = '7.3.0' + ext.airship_framework_proxy_version = '10.1.1' repositories { @@ -45,19 +43,17 @@ android { lintOptions { disable 'InvalidPackage' } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } - namespace = 'com.airship.airship' -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - jvmTarget = "17" + jvmTarget = '17' } + + namespace = 'com.airship.airship' } dependencies { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 510dabb4..1c84c693 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -3,5 +3,36 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt index 7e84ce19..affaa021 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPlugin.kt @@ -3,13 +3,16 @@ package com.airship.flutter import android.app.Activity import android.content.Context import android.os.Build +import android.util.Log import com.urbanairship.actions.ActionResult -import com.urbanairship.android.framework.proxy.Event import com.urbanairship.android.framework.proxy.EventType import com.urbanairship.android.framework.proxy.events.EventEmitter import com.urbanairship.android.framework.proxy.proxies.AirshipProxy import com.urbanairship.android.framework.proxy.proxies.FeatureFlagProxy +import com.urbanairship.android.framework.proxy.proxies.LiveUpdateRequest +import com.urbanairship.android.framework.proxy.proxies.EnableUserNotificationsArgs import com.urbanairship.json.JsonValue +import com.urbanairship.json.toJsonList import io.flutter.embedding.engine.FlutterShellArgs import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -25,6 +28,8 @@ import io.flutter.plugin.platform.PlatformViewRegistry import kotlinx.coroutines.* import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock +import kotlinx.coroutines.Dispatchers + class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -109,6 +114,7 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { override fun onMethodCall(call: MethodCall, result: Result) { val proxy = AirshipProxy.shared(context) + val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) when (call.method) { // Flutter @@ -116,6 +122,7 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { // Airship "takeOff" -> result.resolveResult(call) { proxy.takeOff(call.jsonArgs()) } + "isFlying" -> result.resolveResult(call) { proxy.isFlying() } // Channel @@ -153,9 +160,32 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { // Push "push#setUserNotificationsEnabled" -> result.resolveResult(call) { proxy.push.setUserNotificationsEnabled(call.booleanArg()) } - "push#enableUserNotifications" -> result.resolvePending(call) { proxy.push.enableUserPushNotifications() } + "push#enableUserNotifications" -> { + val args = call.jsonArgs() + + val enableArgs = args?.let { + try { + EnableUserNotificationsArgs.fromJson(it) + } catch (e: Exception) { + null + } + } + + coroutineScope.launch { + try { + val enableResult = proxy.push.enableUserPushNotifications(enableArgs) + result.success(enableResult) + } catch (e: Exception) { + result.error("ERROR", "Failed to enable user notifications", e.localizedMessage) + } + } + } "push#isUserNotificationsEnabled" -> result.resolveResult(call) { proxy.push.isUserNotificationsEnabled() } - "push#getNotificationStatus" -> result.resolveResult(call) { proxy.push.getNotificationStatus() } + "push#getNotificationStatus" -> result.resolveResult(call) { + coroutineScope.launch { + proxy.push.getNotificationStatus() + } + } "push#getActiveNotifications" -> result.resolveResult(call) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { proxy.push.getActiveNotifications() @@ -197,6 +227,8 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "messageCenter#getMessages" -> result.resolveResult(call) { JsonValue.wrapOpt(proxy.messageCenter.getMessages()) } + "messageCenter#showMessageCenter" -> result.resolveResult(call) { proxy.messageCenter.showMessageCenter(call.optStringArg()) } + "messageCenter#showMessageView" -> result.resolveResult(call) { proxy.messageCenter.showMessageView(call.stringArg()) } "messageCenter#display" -> result.resolveResult(call) { proxy.messageCenter.display(call.optStringArg()) } "messageCenter#markMessageRead" -> result.resolveResult(call) { proxy.messageCenter.markMessageRead(call.stringArg()) } "messageCenter#deleteMessage" -> result.resolveResult(call) { proxy.messageCenter.deleteMessage(call.stringArg()) } @@ -248,6 +280,121 @@ class AirshipPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } + // Live Activities + + "featureFlagManager#trackInteraction" -> { + result.resolveDeferred(call) { callback -> + scope.launch { + try { + val args = call.jsonArgs() + + val wrapped = JsonValue.wrap(args) + val featureFlagProxy = FeatureFlagProxy(wrapped) + proxy.featureFlagManager.trackInteraction(flag = featureFlagProxy) + callback(null, null) + } catch (e: Exception) { + callback(null, e) + } + } + } + } + + "liveUpdate#start" -> result.resolveResult(call) { + try { + val args = call.jsonArgs() + Log.d("AirshipPlugin", "Received args for liveUpdate#start: $args") + + val startRequest = LiveUpdateRequest.Start.fromJson(args) + + Log.d("AirshipPlugin", "Created LiveUpdateRequest.Start: $startRequest") + + proxy.liveUpdateManager.start(startRequest) + Log.d("AirshipPlugin", "LiveUpdate start method called successfully") + + null // Return null as the start function doesn't return anything + } catch (e: Exception) { + throw e + } + } + + "liveUpdate#update" -> result.resolveResult(call) { + try { + val args = call.jsonArgs() + Log.d("AirshipPlugin", "Received args for liveUpdate#update: $args") + + val updateRequest = LiveUpdateRequest.Update.fromJson(args) + + proxy.liveUpdateManager.update(updateRequest) + Log.d("AirshipPlugin", "LiveUpdate update method called successfully") + null + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error processing liveUpdate#update request", e) + throw e + } + } + + "liveUpdate#list" -> result.resolveDeferred(call) { callback -> + try { + val args = call.jsonArgs() + Log.d("AirshipPlugin", "Received args for liveUpdate#list: $args") + + val listRequest = LiveUpdateRequest.List.fromJson(args) + + coroutineScope.launch { + try { + val liveUpdates = proxy.liveUpdateManager.list(listRequest) + Log.d("AirshipPlugin", "LiveUpdate list method completed successfully") + callback(liveUpdates.toJsonList(), null) + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error listing LiveUpdates", e) + callback(null, e) + } + } + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error processing liveUpdate#list request", e) + callback(null, e) + } + } + + "liveUpdate#listAll" -> result.resolveDeferred(call) { callback -> + coroutineScope.launch { + try { + val liveUpdates = proxy.liveUpdateManager.listAll() + Log.d("AirshipPlugin", "LiveUpdate listAll method completed successfully") + callback(JsonValue.wrap(liveUpdates), null) + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error listing all LiveUpdates", e) + callback(null, e) + } + } + } + + "liveUpdate#end" -> result.resolveResult(call) { + try { + val args = call.jsonArgs() + Log.d("AirshipPlugin", "Received args for liveUpdate#end: $args") + + val endRequest = LiveUpdateRequest.End.fromJson(args) + + proxy.liveUpdateManager.end(endRequest) + Log.d("AirshipPlugin", "LiveUpdate end method called successfully") + null + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error processing liveUpdate#end request", e) + throw e + } + } + + "liveUpdate#clearAll" -> result.resolveResult(call) { + try { + proxy.liveUpdateManager.clearAll() + Log.d("AirshipPlugin", "LiveUpdate clearAll method called successfully") + null + } catch (e: Exception) { + Log.e("AirshipPlugin", "Error processing liveUpdate#clearAll request", e) + throw e + } + } // Feature Flag "featureFlagManager#flag" -> result.resolveDeferred(call) { callback -> diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPluginExtender.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPluginExtender.kt new file mode 100644 index 00000000..0379fc48 --- /dev/null +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPluginExtender.kt @@ -0,0 +1,16 @@ +/* Copyright Urban Airship and Contributors */ + +package com.urbanairship.reactnative + +import android.content.Context +import com.urbanairship.UAirship + +/** + * Extender that will be called during takeOff to customize the airship instance. + * Register the extender fully qualified class name in the manifest under the key + * `com.urbanairship.flutter.AIRSHIP_EXTENDER`. + */ +@Deprecated("Use com.urbanairship.android.framework.proxy.AirshipPluginExtender instead and register it under the manifest key `com.urbanairship.plugin.extender`") +interface AirshipExtender { + fun onAirshipReady(context: Context, airship: UAirship) +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/airship/flutter/AirshipPluginVersion.kt b/android/src/main/kotlin/com/airship/flutter/AirshipPluginVersion.kt index 4c3b2459..b6898be8 100644 --- a/android/src/main/kotlin/com/airship/flutter/AirshipPluginVersion.kt +++ b/android/src/main/kotlin/com/airship/flutter/AirshipPluginVersion.kt @@ -2,6 +2,6 @@ package com.airship.flutter class AirshipPluginVersion { companion object { - const val AIRSHIP_PLUGIN_VERSION = "7.8.2" + const val AIRSHIP_PLUGIN_VERSION = "7.9.0" } } diff --git a/android/src/main/kotlin/com/airship/flutter/FlutterAutopilot.kt b/android/src/main/kotlin/com/airship/flutter/FlutterAutopilot.kt index ea783eb3..16d58416 100644 --- a/android/src/main/kotlin/com/airship/flutter/FlutterAutopilot.kt +++ b/android/src/main/kotlin/com/airship/flutter/FlutterAutopilot.kt @@ -29,9 +29,7 @@ class FlutterAutopilot : BaseAutopilot() { private val appContext: Context get() = UAirship.getApplicationContext() - override fun onAirshipReady(airship: UAirship) { - super.onAirshipReady(airship) - + override fun onReady(context: Context, airship: UAirship) { Log.i("FlutterAutopilot", "onAirshipReady") // If running in the background, start the background Isolate @@ -66,7 +64,6 @@ class FlutterAutopilot : BaseAutopilot() { } } - companion object { private val APP_KEY = stringPreferencesKey("app_key") private val APP_SECRET = stringPreferencesKey("app_secret") diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..40b66d58 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + constant_identifier_names: false + prefer_const_constructors: false +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index f29076ce..fba1faf5 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -45,6 +45,15 @@ android { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + buildTypes { release { // TODO: Add your own signing config for the release build. diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index b0a48172..6643e163 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,11 @@ + + + { + + if (event == LiveUpdateEvent.END) { + // Dismiss the live update on END. The default behavior will leave the Live Update + // in the notification tray until the dismissal time is reached or the user dismisses it. + return LiveUpdateResult.cancel() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("emoji-example", "Emoji example", importance) + channel.description = "Emoji example" + NotificationManagerCompat.from(context).createNotificationChannel(channel) + } + + val launchIntent = context.packageManager + .getLaunchIntentForPackage(context.packageName) + ?.addCategory(update.name) + ?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + ?.setPackage(null) + + val contentIntent = PendingIntent.getActivity( + context, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, "emoji-example") + .setSmallIcon(R.drawable.ic_notification) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_EVENT) + .setContentTitle("Example Live Update") + .setContentText(update.content.requireField("emoji")) + .setContentIntent(contentIntent) + + return LiveUpdateResult.ok(notification) + } +} \ No newline at end of file diff --git a/example/android/app/src/main/res/drawable/ic_notification.xml b/example/android/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..a8b409b1 --- /dev/null +++ b/example/android/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index b37a815b..fbc5d6f9 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ plugins { - id "com.android.application" version "8.2.2" apply false + id "com.android.application" version '8.6.1' apply false id "org.jetbrains.kotlin.android" version "1.9.0" apply false id "com.google.gms.google-services" version "4.3.14" apply false } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index b4c0361d..8f9af135 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jul 03 16:09:47 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/settings.gradle b/example/android/settings.gradle index f953ebb5..cd268768 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.2" apply false + id "com.android.application" version '8.6.1' apply false id "org.jetbrains.kotlin.android" version "1.9.0" apply false } diff --git a/example/ios/AirshipExampleNotificationServiceExtension/Info.plist b/example/ios/AirshipExampleNotificationServiceExtension/Info.plist new file mode 100644 index 00000000..57421ebf --- /dev/null +++ b/example/ios/AirshipExampleNotificationServiceExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/example/ios/AirshipExampleNotificationServiceExtension/NotificationService.swift b/example/ios/AirshipExampleNotificationServiceExtension/NotificationService.swift new file mode 100644 index 00000000..36b17ca8 --- /dev/null +++ b/example/ios/AirshipExampleNotificationServiceExtension/NotificationService.swift @@ -0,0 +1,6 @@ +/* Copyright Airship and Contributors */ + +import UserNotifications +import AirshipServiceExtension + +class NotificationService: UANotificationServiceExtension {} diff --git a/example/ios/ExampleWidgets/Assets.xcassets/AccentColor.colorset/Contents.json b/example/ios/ExampleWidgets/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/example/ios/ExampleWidgets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/ExampleWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/ExampleWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/example/ios/ExampleWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/ExampleWidgets/Assets.xcassets/Contents.json b/example/ios/ExampleWidgets/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/example/ios/ExampleWidgets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/ExampleWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/example/ios/ExampleWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/example/ios/ExampleWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/ExampleWidgets/ExampleWidgetsAttributes.swift b/example/ios/ExampleWidgets/ExampleWidgetsAttributes.swift new file mode 100644 index 00000000..632913f8 --- /dev/null +++ b/example/ios/ExampleWidgets/ExampleWidgetsAttributes.swift @@ -0,0 +1,13 @@ +/* Copyright Airship and Contributors */ + +import ActivityKit + +struct ExampleWidgetsAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + // Dynamic stateful properties about your activity go here! + var emoji: String + } + + // Fixed non-changing properties about your activity go here! + var name: String +} diff --git a/example/ios/ExampleWidgets/ExampleWidgetsBundle.swift b/example/ios/ExampleWidgets/ExampleWidgetsBundle.swift new file mode 100644 index 00000000..724ea47e --- /dev/null +++ b/example/ios/ExampleWidgets/ExampleWidgetsBundle.swift @@ -0,0 +1,11 @@ +/* Copyright Airship and Contributors */ + +import WidgetKit +import SwiftUI + +@main +struct ExampleWidgetsBundle: WidgetBundle { + var body: some Widget { + ExampleWidgetsLiveActivity() + } +} diff --git a/example/ios/ExampleWidgets/ExampleWidgetsLiveActivity.swift b/example/ios/ExampleWidgets/ExampleWidgetsLiveActivity.swift new file mode 100644 index 00000000..2e8e873b --- /dev/null +++ b/example/ios/ExampleWidgets/ExampleWidgetsLiveActivity.swift @@ -0,0 +1,66 @@ +/* Copyright Airship and Contributors */ + +import ActivityKit +import WidgetKit +import SwiftUI + + +struct ExampleWidgetsLiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: ExampleWidgetsAttributes.self) { context in + // Lock screen/banner UI goes here + VStack { + Text("Hello \(context.state.emoji)") + } + .activityBackgroundTint(Color.cyan) + .activitySystemActionForegroundColor(Color.black) + + } dynamicIsland: { context in + DynamicIsland { + // Expanded UI goes here. Compose the expanded UI through + // various regions, like leading/trailing/center/bottom + DynamicIslandExpandedRegion(.leading) { + Text("Leading") + } + DynamicIslandExpandedRegion(.trailing) { + Text("Trailing") + } + DynamicIslandExpandedRegion(.bottom) { + Text("Bottom \(context.state.emoji)") + // more content + } + } compactLeading: { + Text("L") + } compactTrailing: { + Text("T \(context.state.emoji)") + } minimal: { + Text(context.state.emoji) + } + .widgetURL(URL(string: "http://www.apple.com")) + .keylineTint(Color.red) + } + } +} + +extension ExampleWidgetsAttributes { + fileprivate static var preview: ExampleWidgetsAttributes { + ExampleWidgetsAttributes(name: "World") + } +} + +extension ExampleWidgetsAttributes.ContentState { + fileprivate static var smiley: ExampleWidgetsAttributes.ContentState { + ExampleWidgetsAttributes.ContentState(emoji: "😀") + } + + fileprivate static var starEyes: ExampleWidgetsAttributes.ContentState { + ExampleWidgetsAttributes.ContentState(emoji: "🤩") + } +} + +#Preview("Notification", as: .content, using: ExampleWidgetsAttributes.preview) { + ExampleWidgetsLiveActivity() +} contentStates: { + ExampleWidgetsAttributes.ContentState.smiley + ExampleWidgetsAttributes.ContentState.starEyes +} diff --git a/example/ios/ExampleWidgets/Info.plist b/example/ios/ExampleWidgets/Info.plist new file mode 100644 index 00000000..0f118fb7 --- /dev/null +++ b/example/ios/ExampleWidgets/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/example/ios/Podfile b/example/ios/Podfile index f17bddc9..2580de97 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '15.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,3 +39,9 @@ post_install do |installer| flutter_additional_ios_build_settings(target) end end + +target 'AirshipExampleNotificationServiceExtension' do + use_frameworks! + use_modular_headers! + pod 'AirshipServiceExtension' +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 8217437a..f945f155 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,43 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 17E3C866CA3FCD6568C3185B /* Pods_AirshipExampleNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 060DD0FFEC6F473C0C1B1B41 /* Pods_AirshipExampleNotificationServiceExtension.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A5B8E4E55C3DA257D8B2783B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EBAC940BA18FD7F92E459C9 /* Pods_Runner.framework */; }; + 99041B732CC1BD3400031558 /* AirshipPluginExtender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041B722CC1BD3400031558 /* AirshipPluginExtender.swift */; }; + 99041C022CC1DAE500031558 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 99041C012CC1DAE500031558 /* WidgetKit.framework */; }; + 99041C042CC1DAE500031558 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 99041C032CC1DAE500031558 /* SwiftUI.framework */; }; + 99041C072CC1DAE500031558 /* ExampleWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041C062CC1DAE500031558 /* ExampleWidgetsBundle.swift */; }; + 99041C092CC1DAE500031558 /* ExampleWidgetsLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041C082CC1DAE500031558 /* ExampleWidgetsLiveActivity.swift */; }; + 99041C0F2CC1DAE600031558 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 99041C0E2CC1DAE600031558 /* Assets.xcassets */; }; + 99041C132CC1DAE600031558 /* ExampleWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 99041C002CC1DAE500031558 /* ExampleWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 99041C1B2CC1DB2900031558 /* ExampleWidgetsAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041C192CC1DB1D00031558 /* ExampleWidgetsAttributes.swift */; }; + 99041C232CC29E0300031558 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041C222CC29E0300031558 /* NotificationService.swift */; }; + 99041C272CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 99041C202CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 991A20A62CC5807D0025F15C /* ExampleWidgetsAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99041C192CC1DB1D00031558 /* ExampleWidgetsAttributes.swift */; }; + F2752F00AE0F784D37990F4D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB5811C302D62694114284C7 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 99041C112CC1DAE600031558 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 99041BFF2CC1DAE500031558; + remoteInfo = ExampleWidgetsExtension; + }; + 99041C252CC29E0300031558 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 99041C1F2CC29E0300031558; + remoteInfo = AirshipExampleNotificationServiceExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -27,19 +56,33 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 99041C182CC1DAE600031558 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 99041C132CC1DAE600031558 /* ExampleWidgetsExtension.appex in Embed Foundation Extensions */, + 99041C272CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 060DD0FFEC6F473C0C1B1B41 /* Pods_AirshipExampleNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AirshipExampleNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 22ED3F085B82D96DD685CBE2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3EBAC940BA18FD7F92E459C9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 45277AEA23C53FC5002AEA21 /* Airship.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Airship.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 49DAB7D490EA653F2FF29FB1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 55C9286D2A474BF41F18325D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8259AB33CF8B567B4B9BD8AA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7B0A7B4D0A9B4489842A61EB /* Pods-AirshipExampleNotificationServiceExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AirshipExampleNotificationServiceExtension.profile.xcconfig"; path = "Target Support Files/Pods-AirshipExampleNotificationServiceExtension/Pods-AirshipExampleNotificationServiceExtension.profile.xcconfig"; sourceTree = ""; }; + 96EFF562C1F44C0363D9859D /* Pods-AirshipExampleNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AirshipExampleNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-AirshipExampleNotificationServiceExtension/Pods-AirshipExampleNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -47,8 +90,22 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D98F489A26588C2B01B017D9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 99041B722CC1BD3400031558 /* AirshipPluginExtender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AirshipPluginExtender.swift; sourceTree = ""; }; + 99041C002CC1DAE500031558 /* ExampleWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ExampleWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 99041C012CC1DAE500031558 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 99041C032CC1DAE500031558 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 99041C062CC1DAE500031558 /* ExampleWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleWidgetsBundle.swift; sourceTree = ""; }; + 99041C082CC1DAE500031558 /* ExampleWidgetsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleWidgetsLiveActivity.swift; sourceTree = ""; }; + 99041C0E2CC1DAE600031558 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 99041C102CC1DAE600031558 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 99041C192CC1DB1D00031558 /* ExampleWidgetsAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleWidgetsAttributes.swift; sourceTree = ""; }; + 99041C202CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AirshipExampleNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 99041C222CC29E0300031558 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 99041C242CC29E0300031558 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB5811C302D62694114284C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DF39D0BC24EC8388007487C4 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + F5C980E6DA765A595DF7C726 /* Pods-AirshipExampleNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AirshipExampleNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-AirshipExampleNotificationServiceExtension/Pods-AirshipExampleNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; + F80EA22C38488422A05FF96F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +113,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A5B8E4E55C3DA257D8B2783B /* Pods_Runner.framework in Frameworks */, + F2752F00AE0F784D37990F4D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99041BFD2CC1DAE500031558 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 99041C042CC1DAE500031558 /* SwiftUI.framework in Frameworks */, + 99041C022CC1DAE500031558 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99041C1D2CC29E0300031558 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 17E3C866CA3FCD6568C3185B /* Pods_AirshipExampleNotificationServiceExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -67,7 +141,10 @@ isa = PBXGroup; children = ( 45277AEA23C53FC5002AEA21 /* Airship.framework */, - 3EBAC940BA18FD7F92E459C9 /* Pods_Runner.framework */, + AB5811C302D62694114284C7 /* Pods_Runner.framework */, + 99041C012CC1DAE500031558 /* WidgetKit.framework */, + 99041C032CC1DAE500031558 /* SwiftUI.framework */, + 060DD0FFEC6F473C0C1B1B41 /* Pods_AirshipExampleNotificationServiceExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -75,9 +152,12 @@ 4A5F3BC20F2F71680F0EA95B /* Pods */ = { isa = PBXGroup; children = ( - 49DAB7D490EA653F2FF29FB1 /* Pods-Runner.debug.xcconfig */, - 8259AB33CF8B567B4B9BD8AA /* Pods-Runner.release.xcconfig */, - D98F489A26588C2B01B017D9 /* Pods-Runner.profile.xcconfig */, + 55C9286D2A474BF41F18325D /* Pods-Runner.debug.xcconfig */, + F80EA22C38488422A05FF96F /* Pods-Runner.release.xcconfig */, + 22ED3F085B82D96DD685CBE2 /* Pods-Runner.profile.xcconfig */, + 96EFF562C1F44C0363D9859D /* Pods-AirshipExampleNotificationServiceExtension.debug.xcconfig */, + F5C980E6DA765A595DF7C726 /* Pods-AirshipExampleNotificationServiceExtension.release.xcconfig */, + 7B0A7B4D0A9B4489842A61EB /* Pods-AirshipExampleNotificationServiceExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -98,6 +178,8 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 99041C052CC1DAE500031558 /* ExampleWidgets */, + 99041C212CC29E0300031558 /* AirshipExampleNotificationServiceExtension */, 97C146EF1CF9000F007C117D /* Products */, 4A5F3BC20F2F71680F0EA95B /* Pods */, 18E3DDFE37410BE22FF694C3 /* Frameworks */, @@ -108,6 +190,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 99041C002CC1DAE500031558 /* ExampleWidgetsExtension.appex */, + 99041C202CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex */, ); name = Products; sourceTree = ""; @@ -123,6 +207,7 @@ 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 99041B722CC1BD3400031558 /* AirshipPluginExtender.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); @@ -136,6 +221,27 @@ name = "Supporting Files"; sourceTree = ""; }; + 99041C052CC1DAE500031558 /* ExampleWidgets */ = { + isa = PBXGroup; + children = ( + 99041C062CC1DAE500031558 /* ExampleWidgetsBundle.swift */, + 99041C082CC1DAE500031558 /* ExampleWidgetsLiveActivity.swift */, + 99041C192CC1DB1D00031558 /* ExampleWidgetsAttributes.swift */, + 99041C0E2CC1DAE600031558 /* Assets.xcassets */, + 99041C102CC1DAE600031558 /* Info.plist */, + ); + path = ExampleWidgets; + sourceTree = ""; + }; + 99041C212CC29E0300031558 /* AirshipExampleNotificationServiceExtension */ = { + isa = PBXGroup; + children = ( + 99041C222CC29E0300031558 /* NotificationService.swift */, + 99041C242CC29E0300031558 /* Info.plist */, + ); + path = AirshipExampleNotificationServiceExtension; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -143,30 +249,70 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 92A7554725E28B35CD26D4E6 /* [CP] Check Pods Manifest.lock */, + 1E091F8DEFEE6AA058F231B9 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, + 99041C182CC1DAE600031558 /* Embed Foundation Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 59437A90479CED8BD3D53907 /* [CP] Embed Pods Frameworks */, + 762FAAF0484562820949CC92 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( + 99041C122CC1DAE600031558 /* PBXTargetDependency */, + 99041C262CC29E0300031558 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + 99041BFF2CC1DAE500031558 /* ExampleWidgetsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 99041C142CC1DAE600031558 /* Build configuration list for PBXNativeTarget "ExampleWidgetsExtension" */; + buildPhases = ( + 99041BFC2CC1DAE500031558 /* Sources */, + 99041BFD2CC1DAE500031558 /* Frameworks */, + 99041BFE2CC1DAE500031558 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ExampleWidgetsExtension; + productName = ExampleWidgetsExtension; + productReference = 99041C002CC1DAE500031558 /* ExampleWidgetsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 99041C1F2CC29E0300031558 /* AirshipExampleNotificationServiceExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 99041C282CC29E0300031558 /* Build configuration list for PBXNativeTarget "AirshipExampleNotificationServiceExtension" */; + buildPhases = ( + FEFEACE62CF044E69FD585AA /* [CP] Check Pods Manifest.lock */, + 99041C1C2CC29E0300031558 /* Sources */, + 99041C1D2CC29E0300031558 /* Frameworks */, + 99041C1E2CC29E0300031558 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AirshipExampleNotificationServiceExtension; + productName = AirshipExampleNotificationServiceExtension; + productReference = 99041C202CC29E0300031558 /* AirshipExampleNotificationServiceExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { @@ -175,6 +321,12 @@ DevelopmentTeam = PGJV57GD94; LastSwiftMigration = 1120; }; + 99041BFF2CC1DAE500031558 = { + CreatedOnToolsVersion = 15.4; + }; + 99041C1F2CC29E0300031558 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -191,6 +343,8 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 99041C1F2CC29E0300031558 /* AirshipExampleNotificationServiceExtension */, + 99041BFF2CC1DAE500031558 /* ExampleWidgetsExtension */, ); }; /* End PBXProject section */ @@ -207,9 +361,46 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 99041BFE2CC1DAE500031558 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 99041C0F2CC1DAE600031558 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99041C1E2CC29E0300031558 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1E091F8DEFEE6AA058F231B9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -226,7 +417,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; - 59437A90479CED8BD3D53907 /* [CP] Embed Pods Frameworks */ = { + 762FAAF0484562820949CC92 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -236,54 +427,56 @@ "${BUILT_PRODUCTS_DIR}/Airship/AirshipKit.framework", "${BUILT_PRODUCTS_DIR}/AirshipFrameworkProxy/AirshipFrameworkProxy.framework", "${BUILT_PRODUCTS_DIR}/airship_flutter/airship_flutter.framework", + "${BUILT_PRODUCTS_DIR}/AirshipServiceExtension/AirshipServiceExtension.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AirshipKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AirshipFrameworkProxy.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/airship_flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AirshipServiceExtension.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 92A7554725E28B35CD26D4E6 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Run Script"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + FEFEACE62CF044E69FD585AA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Run Script"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AirshipExampleNotificationServiceExtension-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -292,13 +485,46 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 991A20A62CC5807D0025F15C /* ExampleWidgetsAttributes.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 99041B732CC1BD3400031558 /* AirshipPluginExtender.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99041BFC2CC1DAE500031558 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 99041C092CC1DAE500031558 /* ExampleWidgetsLiveActivity.swift in Sources */, + 99041C1B2CC1DB2900031558 /* ExampleWidgetsAttributes.swift in Sources */, + 99041C072CC1DAE500031558 /* ExampleWidgetsBundle.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99041C1C2CC29E0300031558 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 99041C232CC29E0300031558 /* NotificationService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 99041C122CC1DAE600031558 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 99041BFF2CC1DAE500031558 /* ExampleWidgetsExtension */; + targetProxy = 99041C112CC1DAE600031558 /* PBXContainerItemProxy */; + }; + 99041C262CC29E0300031558 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 99041C1F2CC29E0300031558 /* AirshipExampleNotificationServiceExtension */; + targetProxy = 99041C252CC29E0300031558 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -361,7 +587,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -373,6 +599,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -383,7 +610,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -448,7 +675,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -498,7 +725,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -511,6 +738,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -522,7 +750,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -546,6 +774,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; @@ -557,7 +786,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -575,6 +804,247 @@ }; name = Release; }; + 99041C152CC1DAE600031558 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ExampleWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ExampleWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.ExampleWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 99041C162CC1DAE600031558 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ExampleWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ExampleWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.ExampleWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 99041C172CC1DAE600031558 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ExampleWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ExampleWidgets; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.ExampleWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + 99041C292CC29E0300031558 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96EFF562C1F44C0363D9859D /* Pods-AirshipExampleNotificationServiceExtension.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirshipExampleNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirshipExampleNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.AirshipExampleNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 99041C2A2CC29E0300031558 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F5C980E6DA765A595DF7C726 /* Pods-AirshipExampleNotificationServiceExtension.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirshipExampleNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirshipExampleNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.AirshipExampleNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 99041C2B2CC29E0300031558 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7B0A7B4D0A9B4489842A61EB /* Pods-AirshipExampleNotificationServiceExtension.profile.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PGJV57GD94; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirshipExampleNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirshipExampleNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 The Chromium Authors. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.urbanairship.richpush.AirshipExampleNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -598,6 +1068,26 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 99041C142CC1DAE600031558 /* Build configuration list for PBXNativeTarget "ExampleWidgetsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 99041C152CC1DAE600031558 /* Debug */, + 99041C162CC1DAE600031558 /* Release */, + 99041C172CC1DAE600031558 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 99041C282CC29E0300031558 /* Build configuration list for PBXNativeTarget "AirshipExampleNotificationServiceExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 99041C292CC29E0300031558 /* Debug */, + 99041C2A2CC29E0300031558 /* Release */, + 99041C2B2CC29E0300031558 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/example/ios/Runner/AirshipPluginExtender.swift b/example/ios/Runner/AirshipPluginExtender.swift new file mode 100644 index 00000000..00ea2c95 --- /dev/null +++ b/example/ios/Runner/AirshipPluginExtender.swift @@ -0,0 +1,27 @@ +/* Copyright Airship and Contributors */ + +import Foundation +import AirshipKit +import AirshipFrameworkProxy +import ActivityKit + +@objc(AirshipPluginExtender) +public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol { + + /// Called on the same run loop when Airship takesOff. + @MainActor + public static func onAirshipReady() { + + if #available(iOS 16.1, *) { + // Throws if setup is called more than once + try? LiveActivityManager.shared.setup { configurator in + + // Register each activity type + await configurator.register(forType: Activity.self) { attributes in + // Track this property as the Airship name for updates + attributes.name + } + } + } + } +} diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 9da429a9..7039186a 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,8 +1,10 @@ +/* Copyright Airship and Contributors */ + import UIKit import Flutter import AirshipKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 3b590425..41e5284d 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -50,5 +50,7 @@ UIApplicationSupportsIndirectInputEvents + NSSupportsLiveActivities + diff --git a/example/lib/main.dart b/example/lib/main.dart index 0eb1ff40..7024aebc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:airship_example/styles.dart'; import 'package:flutter/services.dart' show DeviceOrientation, SystemChrome; import 'package:airship_example/screens/home.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; // Supported deep links @@ -28,6 +29,10 @@ void main() { ]); var config = AirshipConfig( + androidConfig: AndroidConfig( + notificationConfig: AndroidNotificationConfig( + icon: "ic_notification", + )), defaultEnvironment: ConfigEnvironment( appKey: "APP_KEY", appSecret: "APP_SECRET", @@ -50,7 +55,10 @@ void main() { } class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override + // ignore: library_private_types_in_public_api _MyAppState createState() => _MyAppState(); } @@ -146,7 +154,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { Airship.messageCenter.onDisplay.listen((event) { key.currentState - ?.push(MaterialPageRoute(builder: (BuildContext context) { + ?.push(MaterialPageRoute(builder: (BuildContext context) { return event.messageId != null ? MessageView( messageId: event.messageId ?? "", @@ -197,7 +205,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { unselectedLabelColor: Colors.grey, // Set unselected label color labelColor: Styles.airshipBlue, // Set selected label color to airshipBlue - tabs: [ + tabs: const [ Tab( icon: Icon(Icons.home), ), @@ -221,17 +229,18 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { Widget tabBarView() { return PopScope( + // ignore: deprecated_member_use onPopInvoked: null, child: Scaffold( backgroundColor: Styles.borders, body: TabBarView( - children: [ + controller: controller, + children: const [ Home(), MessageCenter(), PreferenceCenter(), Settings() ], - controller: controller, ), bottomNavigationBar: bottomNavigationBar(), ), diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 8da2d8fc..2495a63c 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:airship_example/styles.dart'; import 'package:airship_example/widgets/notifications_enabled_button.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; +import 'dart:io' show Platform; +import 'package:uuid/uuid.dart'; class Home extends StatefulWidget { + const Home({super.key}); + @override - _HomeState createState() => _HomeState(); + HomeState createState() => HomeState(); } -class _HomeState extends State { +class HomeState extends State { @override void initState() { initAirshipListeners(); @@ -26,6 +31,123 @@ class _HomeState extends State { }); } + Future _startNewActivity() async { + if (Platform.isIOS) { + LiveActivityStartRequest startRequest = LiveActivityStartRequest( + attributesType: 'ExampleWidgetsAttributes', + attributes: { + "name": Uuid().v4(), + }, + content: + LiveActivityContent(state: {'emoji': '🙌'}, relevanceScore: 0.0)); + + await Airship.liveActivityManager.start(startRequest); + } else if (Platform.isAndroid) { + LiveUpdateStartRequest createRequest = LiveUpdateStartRequest( + name: "Cool", + type: 'Example', + content: {'emoji': '🙌'}, + ); + + await Airship.liveUpdateManager.start(createRequest); + } + } + + Future _stopAllActivities() async { + if (Platform.isIOS) { + List activities = + await Airship.liveActivityManager.listAll(); + for (var activity in activities) { + LiveActivityStopRequest stopRequest = LiveActivityStopRequest( + attributesType: 'ExampleWidgetsAttributes', + activityId: activity.id, + dismissalPolicy: LiveActivityDismissalPolicyImmediate(), + ); + + await Airship.liveActivityManager.end(stopRequest); + } + } else if (Platform.isAndroid) { + List updates = await Airship.liveUpdateManager.listAll(); + for (var update in updates) { + LiveUpdateEndRequest stopRequest = + LiveUpdateEndRequest(name: update.name); + await Airship.liveUpdateManager.end(stopRequest); + } + } + } + + Future _updateAllActivities() async { + if (Platform.isIOS) { + List activities = + await Airship.liveActivityManager.listAll(); + for (var activity in activities) { + LiveActivityContent content = + LiveActivityContent(state: {'emoji': '🙌'}, relevanceScore: 0.0); + + LiveActivityUpdateRequest updateRequest = LiveActivityUpdateRequest( + attributesType: 'ExampleWidgetsAttributes', + activityId: activity.id, + content: content, + ); + + await Airship.liveActivityManager.update(updateRequest); + } + } else if (Platform.isAndroid) { + List updates = await Airship.liveUpdateManager.listAll(); + + for (var update in updates) { + var currentEmoji = update.content['emoji'] ?? ''; + + LiveUpdateUpdateRequest request = LiveUpdateUpdateRequest( + name: update.name, + content: { + 'emoji': currentEmoji == '🙌' ? '👍' : '🙌', + }, + ); + + await Airship.liveUpdateManager.update(request); + } + } + } + + Widget _buildTableRow( + String label, String buttonText, VoidCallback onPressed) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.withAlpha(5), // Slightly lighter than the background + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(left: 16), // Offset label by 16 points + child: Text( + label, + style: TextStyle(color: Colors.white), + ), + ), + Padding( + padding: EdgeInsets.only( + right: 16), // Padding button 16 points from the right + child: SizedBox( + width: 100, // Fixed button width for uniform size + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Styles.airshipBlue.withAlpha(80), + ), + child: Text(buttonText), + ), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -49,7 +171,11 @@ class _HomeState extends State { enableNotificationsButton = Center(child: NotificationsEnabledButton( onPressed: () { - Airship.push.setUserNotificationsEnabled(true); + Airship.push.enableUserNotifications( + options: EnableUserPushNotificationsArgs( + fallback: PromptPermissionFallback.systemSettings, + )); + setState(() {}); }, )); @@ -71,6 +197,38 @@ class _HomeState extends State { }, ), ), + SizedBox(height: 36), + Center( + child: Card( + color: Colors.grey.withAlpha(15), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Platform.isIOS ? 'Live Activities' : 'Live Updates', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 12), + Column( + children: [ + _buildTableRow( + 'Start New', 'Start', _startNewActivity), + _buildTableRow( + 'End All', 'End', _stopAllActivities), + _buildTableRow( + 'Update All', 'Update', _updateAllActivities), + ], + ), + ], + ), + ), + ), + ), ]), ), )); diff --git a/example/lib/screens/message_center.dart b/example/lib/screens/message_center.dart index 4b4ac22f..50b7d240 100644 --- a/example/lib/screens/message_center.dart +++ b/example/lib/screens/message_center.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; import 'package:airship_example/styles.dart'; import 'package:airship_example/screens/message_view.dart'; class MessageCenter extends StatefulWidget { + const MessageCenter({super.key}); + @override - _MessageCenterState createState() => _MessageCenterState(); + MessageCenterState createState() => MessageCenterState(); } -class _MessageCenterState extends State { +class MessageCenterState extends State { @override void initState() { initAirshipListeners(); @@ -31,7 +34,7 @@ class _MessageCenterState extends State { @override Widget build(BuildContext context) { - Widget _buildMessageList(final List messages) { + Widget buildMessageList(final List messages) { return RefreshIndicator( strokeWidth: 1, displacement: 50, @@ -54,13 +57,12 @@ class _MessageCenterState extends State { // Add stream to check isRead child: ListTile( title: message.isRead - ? Text('${message.title}') - : Text('${message.title}', + ? Text(message.title) + : Text(message.title, style: TextStyle(fontWeight: FontWeight.bold)), subtitle: Text('${message.sentDate}'), - leading: Icon(message.isRead - ? Icons.check_circle - : Icons.markunread), + leading: Icon( + message.isRead ? Icons.check_circle : Icons.markunread), onTap: () { Navigator.push( context, @@ -94,7 +96,7 @@ class _MessageCenterState extends State { bottom: false, child: Column( children: [ - Expanded(child: _buildMessageList(list)), + Expanded(child: buildMessageList(list)), ], ), ); diff --git a/example/lib/screens/message_view.dart b/example/lib/screens/message_view.dart index 0f5689bd..afc66705 100644 --- a/example/lib/screens/message_view.dart +++ b/example/lib/screens/message_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; import 'package:airship_example/styles.dart'; import 'package:flutter/services.dart'; @@ -7,13 +8,13 @@ import 'package:collection/collection.dart'; class MessageView extends StatefulWidget { final String messageId; - MessageView({required this.messageId}); + const MessageView({super.key, required this.messageId}); @override - _MessageViewState createState() => _MessageViewState(); + MessageViewState createState() => MessageViewState(); } -class _MessageViewState extends State { +class MessageViewState extends State { bool isLoading = true; @override @@ -38,7 +39,7 @@ class _MessageViewState extends State { return Scaffold( appBar: AppBar( - title: message != null ? Text("${message.title}") : Container(), + title: message != null ? Text(message.title) : Container(), backgroundColor: Styles.background, ), body: Stack(children: [ @@ -82,7 +83,7 @@ class _MessageViewState extends State { builder: (context) => AlertDialog( title: Text( e.message != null ? e.message! : "Unable to load message"), - content: Text(e.details != null ? e.details : ""), + content: Text(e.details ?? ""), )); }); } diff --git a/example/lib/screens/named_user_add.dart b/example/lib/screens/named_user_add.dart index efd6c0a1..c2952e19 100644 --- a/example/lib/screens/named_user_add.dart +++ b/example/lib/screens/named_user_add.dart @@ -1,58 +1,51 @@ import 'package:flutter/material.dart'; import 'package:airship_example/styles.dart'; import 'package:airship_example/widgets/text_add_bar.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; class NamedUserAdd extends StatefulWidget { - final updateParent; + final VoidCallback updateParent; - NamedUserAdd({this.updateParent}); + const NamedUserAdd({Key? key, required this.updateParent}) : super(key: key); @override - _NamedUserAddState createState() => - _NamedUserAddState(updateParent: updateParent); + NamedUserAddState createState() => NamedUserAddState(); } -class _NamedUserAddState extends State { - final updateParent; - - _NamedUserAddState({this.updateParent}); - +class NamedUserAddState extends State { @override void initState() { - Airship.analytics.trackScreen('Add Named User'); super.initState(); + Airship.analytics.trackScreen('Add Named User'); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text("Add Named User"), - backgroundColor: Styles.background, - ), - body: FutureBuilder( - future: Airship.contact.namedUserId, - builder: (context, snapshot) { - return SafeArea( - bottom: false, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextAddBar( - label: snapshot.hasData ? snapshot.data! : "Not set", - onTap: (text) { - Airship.contact.identify(text); - updateParent(); - Navigator.pop(context); - }, - ), - ), - ], + appBar: AppBar( + title: const Text("Add Named User"), + backgroundColor: Styles.background, + ), + body: FutureBuilder( + future: Airship.contact.namedUserId, + builder: (context, snapshot) { + return SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: TextAddBar( + label: snapshot.data ?? "Not set", + onTap: (text) { + Airship.contact.identify(text); + widget.updateParent(); + Navigator.pop(context); + }, ), - ); - }, - )); + ), + ); + }, + ), + ); } } diff --git a/example/lib/screens/preference_center.dart b/example/lib/screens/preference_center.dart index 382e736c..695db819 100644 --- a/example/lib/screens/preference_center.dart +++ b/example/lib/screens/preference_center.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; import 'package:flutter_section_list/flutter_section_list.dart'; class PreferenceCenter extends StatefulWidget { + const PreferenceCenter({super.key}); + @override - _PreferenceCenterState createState() => _PreferenceCenterState(); + PreferenceCenterState createState() => PreferenceCenterState(); } -class _PreferenceCenterState extends State +class PreferenceCenterState extends State with SectionAdapterMixin { String preferenceCenterId = "neat"; PreferenceCenterConfig? fullPreferenceCenterConfig; @@ -32,7 +35,9 @@ class _PreferenceCenterState extends State Airship.preferenceCenter.onDisplay.listen((event) {}); Airship.push.onNotificationStatusChanged.listen((event) { - setState(() { isOptedInToNotifications = event.status.isOptedIn; }); + setState(() { + isOptedInToNotifications = event.status.isOptedIn; + }); }); } @@ -67,15 +72,15 @@ class _PreferenceCenterState extends State /// Filtered version of the preference center config based on the conditions /// defined by sections and items. PreferenceCenterConfig? get preferenceCenterConfig { - var state = new PreferenceCenterConditionState(isOptedInToNotifications); + var state = PreferenceCenterConditionState(isOptedInToNotifications); if (fullPreferenceCenterConfig == null) return null; - var sections = fullPreferenceCenterConfig!.sections.where((s) => - s.evaluateConditions(state) - ).map((s) => s.copy( - s.items?.where((i) => i.evaluateConditions(state)).toList() ?? [] - )).toList(); + var sections = fullPreferenceCenterConfig!.sections + .where((s) => s.evaluateConditions(state)) + .map((s) => s.copy( + s.items?.where((i) => i.evaluateConditions(state)).toList() ?? [])) + .toList(); return fullPreferenceCenterConfig!.copy(sections); } @@ -99,8 +104,9 @@ class _PreferenceCenterState extends State } else { return false; } - } else + } else { return false; + } } void onPreferenceChannelItemToggled(String subscriptionId, bool subscribe) { @@ -119,10 +125,11 @@ class _PreferenceCenterState extends State void applyContactSubscription( String subscriptionId, List scopes, bool subscribe) { List currentScopes = - activeContactSubscriptions[subscriptionId] ?? List.empty(growable: true); + activeContactSubscriptions[subscriptionId] ?? + List.empty(growable: true); List newScopes = List.empty(growable: true); if (subscribe) { - newScopes = new List.from(currentScopes)..addAll(scopes); + newScopes = List.from(currentScopes)..addAll(scopes); } else { currentScopes.removeWhere((item) => scopes.contains(item)); newScopes = currentScopes; @@ -266,11 +273,9 @@ class _PreferenceCenterState extends State @override Widget getItem(BuildContext context, IndexPath indexPath) { - return Container( - child: Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [item(indexPath), Divider(height: 0.5)], - ), + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [item(indexPath), Divider(height: 0.5)], ); } @@ -287,9 +292,9 @@ class _PreferenceCenterState extends State return Container( color: Colors.blueGrey, child: ListTile( - title: Text('${preferenceCenterConfig?.display?.title ?? ''}', + title: Text(preferenceCenterConfig?.display?.title ?? '', style: TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text('${preferenceCenterConfig?.display?.subtitle ?? ''}'), + subtitle: Text(preferenceCenterConfig?.display?.subtitle ?? ''), ), ); } @@ -305,10 +310,10 @@ class _PreferenceCenterState extends State color: Colors.cyan, child: ListTile( title: Text( - '${preferenceCenterConfig?.sections[section].display?.title ?? ''}', + preferenceCenterConfig?.sections[section].display?.title ?? '', style: TextStyle(fontWeight: FontWeight.bold)), subtitle: Text( - '${preferenceCenterConfig?.sections[section].display?.subtitle ?? ''}'), + preferenceCenterConfig?.sections[section].display?.subtitle ?? ''), ), ); } diff --git a/example/lib/screens/settings.dart b/example/lib/screens/settings.dart index 014e2bc5..d78cf0cb 100644 --- a/example/lib/screens/settings.dart +++ b/example/lib/screens/settings.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'package:airship_example/screens/tag_add.dart'; import 'package:airship_example/screens/named_user_add.dart'; import 'package:airship_example/styles.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; class Settings extends StatefulWidget { + const Settings({super.key}); + @override - _SettingsState createState() => _SettingsState(); + SettingsState createState() => SettingsState(); } -class _SettingsState extends State { +class SettingsState extends State { @override void initState() { Airship.analytics.trackScreen('Settings'); diff --git a/example/lib/screens/tag_add.dart b/example/lib/screens/tag_add.dart index fc63b8be..60faa7b4 100644 --- a/example/lib/screens/tag_add.dart +++ b/example/lib/screens/tag_add.dart @@ -1,94 +1,88 @@ import 'package:flutter/material.dart'; import 'package:airship_example/styles.dart'; import 'package:airship_example/widgets/text_add_bar.dart'; +// ignore: depend_on_referenced_packages import 'package:airship_flutter/airship_flutter.dart'; class TagAdd extends StatefulWidget { - final updateParent; + final VoidCallback updateParent; - TagAdd({this.updateParent}); + const TagAdd({Key? key, required this.updateParent}) : super(key: key); @override - _TagAddState createState() => _TagAddState(updateParent: updateParent); + TagAddState createState() => TagAddState(); } -class _TagAddState extends State { - final updateParent; - - _TagAddState({this.updateParent}); - +class TagAddState extends State { @override void initState() { - Airship.analytics.trackScreen('Add Tag'); super.initState(); + Airship.analytics.trackScreen('Add Tag'); } @override Widget build(BuildContext context) { - Widget _buildTagList(List tags) { - return ListView.builder( - itemCount: tags.length, - itemBuilder: (context, index) { - var tag = tags[index]; - - return Dismissible( - key: Key(UniqueKey().toString()), - background: Container(color: Styles.airshipRed), - onDismissed: (direction) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("tag \"$tag\" removed"))); - Airship.channel.removeTags([tag]); - updateState(); - }, - child: Card( - elevation: 5.0, - child: ListTile( - title: Text('$tag'), - )), + return Scaffold( + appBar: AppBar( + title: const Text("Add Tag"), + backgroundColor: Styles.background, + ), + body: FutureBuilder>( + future: Airship.channel.tags, + builder: (context, snapshot) { + return SafeArea( + bottom: false, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: TextAddBar( + label: "Add a tag", + onTap: (tagText) { + FocusScope.of(context).unfocus(); + Airship.channel.addTags([tagText]); + _updateState(); + }, + ), + ), + if (snapshot.hasData) + Expanded(child: _buildTagList(snapshot.data!)) + else + const SizedBox.shrink(), + ], + ), ); }, - ); - } - - return Scaffold( - appBar: AppBar( - title: Text("Add Tag"), - backgroundColor: Styles.background, - ), - body: FutureBuilder>( - future: Airship.channel.tags, - builder: (context, snapshot) { - Expanded? expandedList; - - if (snapshot.hasData) { - expandedList = Expanded(child: _buildTagList(snapshot.data!)); - } + ), + ); + } - return SafeArea( - bottom: false, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextAddBar( - label: "Add a tag", - onTap: (tagText) { - FocusScope.of(context).unfocus(); - Airship.channel.addTags([tagText]); - updateState(); - }, - ), - ), - expandedList ?? Container(), - ], - ), + Widget _buildTagList(List tags) { + return ListView.builder( + itemCount: tags.length, + itemBuilder: (context, index) { + final tag = tags[index]; + return Dismissible( + key: ValueKey(tag), + background: Container(color: Styles.airshipRed), + onDismissed: (_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('tag "$tag" removed')), ); + Airship.channel.removeTags([tag]); + _updateState(); }, - )); + child: Card( + elevation: 5.0, + child: ListTile(title: Text(tag)), + ), + ); + }, + ); } - updateState() { - updateParent(); + void _updateState() { + widget.updateParent(); setState(() {}); } } diff --git a/example/lib/widgets/notifications_enabled_button.dart b/example/lib/widgets/notifications_enabled_button.dart index ca8b2e13..33ddf069 100644 --- a/example/lib/widgets/notifications_enabled_button.dart +++ b/example/lib/widgets/notifications_enabled_button.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:airship_example/styles.dart'; -typedef void TapCallback(String text); +typedef TapCallback = void Function(String text); class NotificationsEnabledButton extends StatelessWidget { - final onPressed; + final VoidCallback onPressed; - NotificationsEnabledButton({ + const NotificationsEnabledButton({ + super.key, required this.onPressed, }); @@ -16,15 +17,15 @@ class NotificationsEnabledButton extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: MaterialButton( - child: Text( - "Enable Push", - style: Styles.homeButtonText, - ), color: Styles.airshipRed, shape: StadiumBorder(), height: 40, minWidth: 400, padding: EdgeInsets.symmetric(vertical: 35), - onPressed: onPressed))); + onPressed: onPressed, + child: Text( + "Enable Push", + style: Styles.homeButtonText, + )))); } } diff --git a/example/lib/widgets/text_add_bar.dart b/example/lib/widgets/text_add_bar.dart index 60647dfe..586dde46 100644 --- a/example/lib/widgets/text_add_bar.dart +++ b/example/lib/widgets/text_add_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:airship_example/styles.dart'; import 'package:flutter/services.dart' show SystemChannels; -typedef void TapCallback(String text); +typedef TapCallback = void Function(String text); class TextAddBar extends StatelessWidget { final String label; @@ -11,6 +11,7 @@ class TextAddBar extends StatelessWidget { final focusNode = FocusNode(); TextAddBar({ + super.key, required this.label, required this.onTap, }); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e978a6ab..811368e9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,10 +8,11 @@ environment: sdk: ">=2.17.2 <4.0.0" dependencies: + uuid: ^3.0.7 # For live activities flutter: sdk: flutter flutter_section_list: ^1.1.1 - + collection: ^1.18.0 dev_dependencies: @@ -20,7 +21,7 @@ dev_dependencies: airship_flutter: path: ../ - + flutter_lints: ^2.0.0 # Add this line (use the latest version available) # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8b781980..ba1a9330 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 06 11:27:54 PDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ios/Classes/AirshipAutopilot.swift b/ios/Classes/AirshipAutopilot.swift index 48e1eba0..25e4075b 100644 --- a/ios/Classes/AirshipAutopilot.swift +++ b/ios/Classes/AirshipAutopilot.swift @@ -2,8 +2,6 @@ import Flutter import AirshipKit import AirshipFrameworkProxy - -@objc(FlutterAirshipAutopilot) public class AirshipAutopilot: NSObject { @objc diff --git a/ios/Classes/AirshipPlugin.m b/ios/Classes/AirshipPlugin.m index 3d7e05f5..f0e7bdf7 100644 --- a/ios/Classes/AirshipPlugin.m +++ b/ios/Classes/AirshipPlugin.m @@ -5,13 +5,4 @@ @implementation AirshipPlugin + (void)registerWithRegistrar:(NSObject*)registrar { [SwiftAirshipPlugin registerWithRegistrar:registrar]; } - -+ (void)load { - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center addObserverForName:UIApplicationDidFinishLaunchingNotification - object:nil - queue:nil usingBlock:^(NSNotification * _Nonnull note) { - [FlutterAirshipAutopilot.shared onLoadWithLaunchOptions:note.userInfo]; - }]; -} @end diff --git a/ios/Classes/AirshipPluginLoader.swift b/ios/Classes/AirshipPluginLoader.swift new file mode 100644 index 00000000..d0b48993 --- /dev/null +++ b/ios/Classes/AirshipPluginLoader.swift @@ -0,0 +1,10 @@ +import AirshipFrameworkProxy + +@objc(AirshipPluginLoader) +public class AirshipPluginLoader: NSObject, AirshipPluginLoaderProtocol { + public static func onApplicationDidFinishLaunching( + launchOptions: [UIApplication.LaunchOptionsKey : Any]? + ) { + AirshipAutopilot.shared.onLoad(launchOptions: launchOptions) + } +} diff --git a/ios/Classes/AirshipPluginVersion.swift b/ios/Classes/AirshipPluginVersion.swift index 59d4c16c..6f9e4a3e 100644 --- a/ios/Classes/AirshipPluginVersion.swift +++ b/ios/Classes/AirshipPluginVersion.swift @@ -1,5 +1,5 @@ import Foundation class AirshipPluginVersion { - static let pluginVersion = "7.8.2" + static let pluginVersion = "7.9.0" } diff --git a/ios/Classes/SwiftAirshipPlugin.swift b/ios/Classes/SwiftAirshipPlugin.swift index 1980f4c1..60c2899f 100644 --- a/ios/Classes/SwiftAirshipPlugin.swift +++ b/ios/Classes/SwiftAirshipPlugin.swift @@ -87,7 +87,7 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { } } } - + @MainActor private func handle(_ call: FlutterMethodCall) async throws -> Any? { switch call.method { @@ -204,8 +204,18 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { return nil case "push#enableUserNotifications": - return try await AirshipProxy.shared.push.enableUserPushNotifications() - + guard let args = try? call.requireAnyArg() as? [String: Any] else { + throw AirshipErrors.error("Invalid argument type. Expected dictionary.") + } + + let json = try AirshipJSON.wrap(args) + + if let enableArgs: EnableUserPushNotificationsArgs = try? json.decode() { + return try? await AirshipProxy.shared.push.enableUserPushNotifications(args: enableArgs) + } else { + return try? await AirshipProxy.shared.push.enableUserPushNotifications() + } + case "push#isUserNotificationsEnabled": return try AirshipProxy.shared.push.isUserNotificationsEnabled() @@ -356,6 +366,19 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { try call.requireBooleanArg() ) return nil + + case "messageCenter#showMessageCenter": + let optionalMessageId = call.arguments as? String + try AirshipProxy.shared.messageCenter.showMessageCenter( + messageID: optionalMessageId + ) + return nil + + case "messageCenter#showMessageView": + try AirshipProxy.shared.messageCenter.showMessageView( + messageID: try call.requireStringArg() + ) + return nil // Preference Center case "preferenceCenter#display": @@ -412,7 +435,6 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { featuresNames: try call.requireStringArrayArg() ) - // Locale case "locale#setLocaleOverride": try AirshipProxy.shared.locale.setCurrentLocale( @@ -444,7 +466,102 @@ public class SwiftAirshipPlugin: NSObject, FlutterPlugin { ) as? AirshipJSON return result?.unWrap() - // Feature Flag + // Live Activity + + case "liveActivity#start": + guard let args = try? call.requireAnyArg() as? [String: Any] else { + throw AirshipErrors.error("Invalid argument type. Expected dictionary.") + } + + + if #available(iOS 16.1, *) { + do { + + let json = try AirshipJSON.wrap(args) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let start:LiveActivityRequest.Start = try json.decode(decoder: decoder) + + let result = try AirshipJSON.wrap(try await LiveActivityManager.shared.start(start)) + + return result.unWrap() + } catch { + throw AirshipErrors.error("Unable to start request: \(error.localizedDescription)") + } + } else { + throw AirshipErrors.error("Not available before iOS 16.1") + } + + case "liveActivity#update": + guard let args = try? call.requireAnyArg() as? [String: Any] else { + throw AirshipErrors.error("Invalid argument type. Expected dictionary.") + } + + + if #available(iOS 16.1, *) { + let json = try AirshipJSON.wrap(args) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let update:LiveActivityRequest.Update = try json.decode(decoder: decoder) + + try await LiveActivityManager.shared.update(update) + + return nil + } else { + throw AirshipErrors.error("Not available before iOS 16.1") + } + case "liveActivity#listAll": + if #available(iOS 16.1, *) { + let result = try await LiveActivityManager.shared.listAll() + return try AirshipJSON.wrap(result).unWrap() as Any + } else { + throw AirshipErrors.error("Not available before 16.1") + } + + case "liveActivity#list": + if #available(iOS 16.1, *) { + let result = try await LiveActivityManager.shared.list(try AirshipJSON.wrap(call.arguments).decode()) + return try AirshipJSON.wrap(result).unWrap() as Any + } else { + throw AirshipErrors.error("Not available before 16.1") + } + case "liveActivity#stop": + guard let args = try? call.requireAnyArg() as? [String: Any] else { + throw AirshipErrors.error("Invalid argument type. Expected dictionary.") + } + + if #available(iOS 16.1, *) { + let json = try AirshipJSON.wrap(args) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let request:LiveActivityRequest.End = try json.decode(decoder: decoder) + + try await LiveActivityManager.shared.end(request) + + return nil + } else { + throw AirshipErrors.error("Not available before iOS 16.1") + } + + case "liveUpdate#start": + throw AirshipErrors.error("Not available on iOS") + + case "liveUpdate#listAll": + throw AirshipErrors.error("Not available on iOS") + + case "liveUpdate#list": + throw AirshipErrors.error("Not available on iOS") + + case "liveUpdate#update": + throw AirshipErrors.error("Not available on iOS") + + case "liveUpdate#end": + throw AirshipErrors.error("Not available on iOS") + + // Feature Flag case "featureFlagManager#flag": let flag = try await AirshipProxy.shared.featureFlagManager.flag( name: try call.requireStringArg() diff --git a/ios/airship_flutter.podspec b/ios/airship_flutter.podspec index d0ffe797..c2913210 100644 --- a/ios/airship_flutter.podspec +++ b/ios/airship_flutter.podspec @@ -1,5 +1,5 @@ -AIRSHIP_FLUTTER_VERSION="7.8.2" +AIRSHIP_FLUTTER_VERSION="7.9.0" # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html @@ -20,6 +20,6 @@ Airship flutter plugin. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.ios.deployment_target = "14.0" - s.dependency "AirshipFrameworkProxy", "7.3.0" + s.dependency "AirshipFrameworkProxy", "10.1.1" end diff --git a/lib/airship_flutter.dart b/lib/airship_flutter.dart index 0a2eb0fb..4f28f0ee 100644 --- a/lib/airship_flutter.dart +++ b/lib/airship_flutter.dart @@ -20,7 +20,6 @@ export 'src/push_notification_status.dart'; export 'src/push_payload.dart'; export 'src/ios_push_options.dart'; - export 'src/attribute_editor.dart'; export 'src/channel_scope.dart'; export 'src/custom_event.dart'; @@ -32,4 +31,7 @@ export 'src/scoped_subscription_list_editor.dart'; export 'src/subscription_list_editor.dart'; export 'src/tag_group_editor.dart'; export 'src/tag_editor.dart'; - +export 'src/live_activity.dart'; +export 'src/live_update.dart'; +export 'src/airship_live_activity_manager.dart'; +export 'src/airship_live_update_manager.dart'; diff --git a/lib/src/airship_flutter.dart b/lib/src/airship_flutter.dart index e50dd904..42f9304d 100644 --- a/lib/src/airship_flutter.dart +++ b/lib/src/airship_flutter.dart @@ -39,6 +39,12 @@ class Airship { /// The [AirshipFeatureFlagManager] instance. static final featureFlagManager = AirshipFeatureFlagManager(_module); + /// The [AirshipLiveActivityManager] instance. + static final liveActivityManager = AirshipLiveActivityManager(_module); + + /// The [AirshipLiveUpdateManager] instance. + static final liveUpdateManager = AirshipLiveUpdateManager(_module); + // /// Initializes Airship with the given config. Airship will store the config /// and automatically use it during the next app init. Any chances to config diff --git a/lib/src/airship_live_activity_manager.dart b/lib/src/airship_live_activity_manager.dart new file mode 100644 index 00000000..d26dfc7c --- /dev/null +++ b/lib/src/airship_live_activity_manager.dart @@ -0,0 +1,48 @@ +import 'airship_module.dart'; +import 'live_activity.dart'; + +/// Live Activity manager. +class AirshipLiveActivityManager { + final AirshipModule _module; + + AirshipLiveActivityManager(AirshipModule module) : _module = module; + + /// Starts a Live Activity. + /// @param request The request options. + /// @returns A Future with the result. + Future start(LiveActivityStartRequest request) async { + var response = await _module.channel + .invokeMethod('liveActivity#start', request.toJson()); + return LiveActivity.fromJson(response); + } + + /// Lists any Live Activities for the request. + /// @param request The request options. + /// @returns A Future with the result. + Future> list(LiveActivityListRequest request) async { + var response = await _module.channel + .invokeMethod('liveActivity#list', request.toJson()); + return (response as List).map((e) => LiveActivity.fromJson(e)).toList(); + } + + /// Lists all Live Activities. + /// @returns A Future with the result. + Future> listAll() async { + var response = await _module.channel.invokeMethod('liveActivity#listAll'); + return (response as List).map((e) => LiveActivity.fromJson(e)).toList(); + } + + /// Updates a Live Activity. + /// @param request The request options. + /// @returns A Future with the result. + Future update(LiveActivityUpdateRequest request) async { + await _module.channel.invokeMethod('liveActivity#update', request.toJson()); + } + + /// End a Live Activity. + /// @param request The request options. + /// @returns A Future with the result. + Future end(LiveActivityStopRequest request) async { + await _module.channel.invokeMethod('liveActivity#stop', request.toJson()); + } +} diff --git a/lib/src/airship_live_update_manager.dart b/lib/src/airship_live_update_manager.dart new file mode 100644 index 00000000..3497698a --- /dev/null +++ b/lib/src/airship_live_update_manager.dart @@ -0,0 +1,64 @@ +import 'airship_module.dart'; +import 'live_update.dart'; + +/// Live Update manager. +class AirshipLiveUpdateManager { + final AirshipModule _module; + + AirshipLiveUpdateManager(this._module); + + /// Creates a Live Update. + /// @param request The request options. + /// @returns A Future with the result. + Future start(LiveUpdateStartRequest request) async { + await _module.channel.invokeMethod('liveUpdate#start', request.toJson()); + } + + /// Updates a Live Update. + /// @param request The request options. + /// @returns A Future with the result. + Future update(LiveUpdateUpdateRequest request) async { + await _module.channel.invokeMethod('liveUpdate#update', request.toJson()); + } + + /// Lists any Live Updates for the request. + /// @param request The request options. + /// @returns A Future with the result. + Future> list(LiveUpdateListRequest request) async { + var response = + await _module.channel.invokeMethod('liveUpdate#list', request.toJson()); + return (response as List) + .map((e) => LiveUpdate.fromJson(e as Map)) + .toList(); + } + + /// Lists all Live Updates. + /// @returns A Future with the result. + // Future> listAll() async { + // var response = await _module.channel.invokeMethod('liveUpdate#listAll'); + // return (response as List) + // .map((e) => LiveUpdate.fromJson(e as Map)) + // .toList(); + // } + + Future> listAll() async { + try { + final result = await _module.channel.invokeMethod('liveUpdate#listAll'); + if (result is List) { + return result.map((item) => LiveUpdate.fromJson(item)).toList(); + } + throw FormatException( + 'Invalid result format: expected a List, got ${result.runtimeType}'); + } catch (e) { + print('Error listing all live updates: $e'); + rethrow; + } + } + + /// End a Live Update. + /// @param request The request options. + /// @returns A Future with the result. + Future end(LiveUpdateEndRequest request) async { + await _module.channel.invokeMethod('liveUpdate#end', request.toJson()); + } +} diff --git a/lib/src/airship_message_center.dart b/lib/src/airship_message_center.dart index 9586870a..6cafed13 100644 --- a/lib/src/airship_message_center.dart +++ b/lib/src/airship_message_center.dart @@ -3,72 +3,109 @@ import 'inbox_message.dart'; import 'airship_events.dart'; class AirshipMessageCenter { - final AirshipModule _module; AirshipMessageCenter(AirshipModule module) : _module = module; - /// Gets the current inbox messages. + /// Retrieves the current list of inbox messages. + /// + /// Returns a Future that resolves to a List of InboxMessage objects. Future> get messages async { - List inboxMessages = await (_module.channel.invokeMethod( - "messageCenter#getMessages")); + List inboxMessages = + await (_module.channel.invokeMethod("messageCenter#getMessages")); return inboxMessages.map((dynamic payload) { return InboxMessage.fromJson(payload); }).toList(); } - /// Marks an inbox message with the [messageId] as read. + /// Marks a specific inbox message as read. + /// + /// [messageId] The unique identifier of the message to be marked as read. + /// Returns a Future that completes when the operation is finished. Future markRead(String messageId) async { - return await _module.channel.invokeMethod( - 'messageCenter#markMessageRead', messageId); + return await _module.channel + .invokeMethod('messageCenter#markMessageRead', messageId); } - /// Requests to display the Message Center. + /// Requests to display the Message Center UI. + /// + /// [messageId] Optional. If provided, opens the Message Center to this specific message. + /// Returns a Future that completes when the display request is processed. Future display([String? messageId]) async { - return await _module.channel.invokeMethod( - 'messageCenter#display', messageId); + return await _module.channel + .invokeMethod('messageCenter#display', messageId); } - /// Deletes an inbox message with the id [messageId]. + /// Deletes a specific inbox message. + /// + /// [messageId] The unique identifier of the message to be deleted. + /// Returns a Future that completes when the deletion is finished. Future deleteMessage(String messageId) async { - return await _module.channel.invokeMethod( - 'messageCenter#deleteMessage', messageId); + return await _module.channel + .invokeMethod('messageCenter#deleteMessage', messageId); } - /// Gets the unread count. + /// Retrieves the count of unread messages in the inbox. + /// + /// Returns a Future that resolves to an integer representing the unread count. Future get unreadMessageCount async { - return await _module.channel.invokeMethod( - 'messageCenter#getUnreadMessageCount'); + return await _module.channel + .invokeMethod('messageCenter#getUnreadMessageCount'); } - /// Enables or disables showing the OOTB UI when requested to display. + /// Configures whether the default Message Center UI should automatically launch when requested. + /// + /// [enabled] If true, enables auto-launch; if false, disables it. + /// Returns a Future that completes when the setting is applied. Future setAutoLaunchDefaultMessageCenter(bool enabled) async { - return await _module.channel.invokeMethod( - 'messageCenter#setAutoLaunch', enabled); + return await _module.channel + .invokeMethod('messageCenter#setAutoLaunch', enabled); } - /// Forces the inbox to refresh. + /// Displays the Message Center UI, overriding the auto-launch setting. /// - /// This is normally not needed as the inbox will automatically refresh on - /// foreground or when a push arrives that's associated with a message. + /// This method will show the Message Center regardless of the auto-launch configuration. + /// [messageId] Optional. If provided, opens the Message Center to this specific message. + /// Returns a Future that completes when the Message Center is displayed. + Future showMessageCenter([String? messageId]) { + return _module.channel + .invokeMethod('messageCenter#showMessageCenter', messageId); + } + + /// Displays a specific message view, overriding the auto-launch setting. + /// + /// This method will show the message view for a specific message, regardless of the auto-launch configuration. + /// [messageId] The unique identifier of the message to display. + /// Returns a Future that completes when the message view is displayed. + Future showMessageView(String messageId) { + return _module.channel + .invokeMethod('messageCenter#showMessageView', messageId); + } + + /// Forces a refresh of the inbox messages. + /// + /// This method is typically not needed as the inbox automatically refreshes on app foreground or when a new message arrives. + /// Returns a Future that resolves to a boolean indicating the success of the refresh operation. Future refreshInbox() async { return _module.channel.invokeMethod("messageCenter#refreshMessages"); } - /// Gets inbox updated event stream. + /// Provides a stream of events for when the inbox is updated. + /// + /// Returns a Stream of MessageCenterUpdatedEvent objects. Stream get onInboxUpdated { return _module .getEventStream("com.airship.flutter/event/message_center_updated") .map((dynamic value) => MessageCenterUpdatedEvent.fromJson(value)); - } - /// Gets show inbox event stream. Events will only be - /// emitted if [setAutoLaunchDefaultMessageCenter] is disabled. + /// Provides a stream of events for when the Message Center should be displayed. + /// + /// This stream will only emit events if auto-launch is disabled via setAutoLaunchDefaultMessageCenter. + /// Returns a Stream of DisplayMessageCenterEvent objects. Stream get onDisplay { return _module .getEventStream("com.airship.flutter/event/display_message_center") .map((dynamic value) => DisplayMessageCenterEvent.fromJson(value)); } } - diff --git a/lib/src/airship_push.dart b/lib/src/airship_push.dart index d6c44798..3b70d0c0 100644 --- a/lib/src/airship_push.dart +++ b/lib/src/airship_push.dart @@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart' hide Notification; import 'airship_utils.dart'; class AirshipPush { - final AirshipModule _module; final IOSPush iOS; final AndroidPush android; @@ -22,25 +21,32 @@ class AirshipPush { /// Tells if user notifications are enabled or not. Future get isUserNotificationsEnabled async { - return await _module.channel.invokeMethod( - 'push#isUserNotificationsEnabled'); + return await _module.channel + .invokeMethod('push#isUserNotificationsEnabled'); } /// Enables or disables the user notifications. Future setUserNotificationsEnabled(bool enabled) async { - return await _module.channel.invokeMethod( - 'push#setUserNotificationsEnabled', enabled); + return await _module.channel + .invokeMethod('push#setUserNotificationsEnabled', enabled); } /// Enables user notifications. - Future enableUserNotifications() async { - return await _module.channel.invokeMethod('push#enableUserNotifications'); + /// + /// [args] Optional arguments for enabling user notifications. + /// + /// Returns a Future with the permission result. The result may be null if the operation fails. + Future enableUserNotifications( + {EnableUserPushNotificationsArgs? options}) async { + final Map arguments = options?.toJson() ?? {}; + return await _module.channel + .invokeMethod('push#enableUserNotifications', arguments); } /// Gets the notification status. Future get notificationStatus async { - var payload = await _module.channel.invokeMethod( - 'push#getNotificationStatus'); + var payload = + await _module.channel.invokeMethod('push#getNotificationStatus'); return PushNotificationStatus.fromJson(Map.from(payload)); } @@ -54,7 +60,7 @@ class AirshipPush { /// Supported on Android Marshmallow (23)+ and iOS 10+. Future> get activeNotifications async { List notifications = - await (_module.channel.invokeMethod('push#getActiveNotifications')); + await (_module.channel.invokeMethod('push#getActiveNotifications')); return notifications.map((dynamic payload) { return PushPayload.fromJson(payload); }).toList(); @@ -64,8 +70,8 @@ class AirshipPush { /// /// The [notification] parameter is the notification ID. Future clearNotification(String notification) async { - return await _module.channel.invokeMethod( - 'push#clearNotification', notification); + return await _module.channel + .invokeMethod('push#clearNotification', notification); } /// Clears all notifications for the application. @@ -99,7 +105,7 @@ class AirshipPush { return _module .getEventStream("com.airship.flutter/event/notification_status_changed") .map((dynamic value) => - PushNotificationStatusChangedEvent.fromJson(value)); + PushNotificationStatusChangedEvent.fromJson(value)); } } @@ -108,8 +114,7 @@ class AndroidPush { final AirshipModule _module; static bool _isBackgroundHandlerSet = false; - AndroidPush(AirshipModule module) - : _module = module; + AndroidPush(AirshipModule module) : _module = module; /// Sets a background message handler. Future setBackgroundPushReceivedHandler( @@ -123,8 +128,7 @@ class AndroidPush { } _isBackgroundHandlerSet = true; - final isolateCallback = - PluginUtilities.getCallbackHandle( + final isolateCallback = PluginUtilities.getCallbackHandle( _androidBackgroundMessageIsolateCallback)!; final messageCallback = PluginUtilities.getCallbackHandle(handler)!; await _module.channel.invokeMapMethod("startBackgroundIsolate", { @@ -134,7 +138,6 @@ class AndroidPush { } } - @pragma('vm:entry-point') void _androidBackgroundMessageIsolateCallback() { WidgetsFlutterBinding.ensureInitialized(); @@ -144,7 +147,7 @@ void _androidBackgroundMessageIsolateCallback() { final args = call.arguments; final handle = CallbackHandle.fromRawHandle(args["messageCallback"]); final callback = PluginUtilities.getCallbackFromHandle(handle) - as AndroidBackgroundPushReceivedHandler; + as AndroidBackgroundPushReceivedHandler; try { final event = PushReceivedEvent.fromJson(args["event"]); await callback(event); @@ -158,23 +161,22 @@ void _androidBackgroundMessageIsolateCallback() { }); // Tell the native side to start the background isolate. - AirshipModule.singleton.backgroundChannel.invokeMethod( - "backgroundIsolateStarted"); + AirshipModule.singleton.backgroundChannel + .invokeMethod("backgroundIsolateStarted"); } /// Specific iOS Push configuration class IOSPush { final AirshipModule _module; - IOSPush(AirshipModule module) - : _module = module; + IOSPush(AirshipModule module) : _module = module; /// Checks if auto-badging is enabled on iOS. Badging is not supported for Android. Future isAutoBadgeEnabled() async { var isAutoBadgeEnabled = false; if (Platform.isIOS) { isAutoBadgeEnabled = - await _module.channel.invokeMethod('push#ios#isAutobadgeEnabled'); + await _module.channel.invokeMethod('push#ios#isAutobadgeEnabled'); } return isAutoBadgeEnabled; } @@ -213,8 +215,8 @@ class IOSPush { } } - return await _module.channel.invokeMethod( - 'push#ios#setNotificationOptions', strings); + return await _module.channel + .invokeMethod('push#ios#setNotificationOptions', strings); } /// Sets the notification options. @@ -242,8 +244,8 @@ class IOSPush { } } - return await _module.channel.invokeMethod( - 'push#ios#setForegroundPresentationOptions', strings); + return await _module.channel + .invokeMethod('push#ios#setForegroundPresentationOptions', strings); } /// Enables or disables auto-badging on iOS. Badging is not supported for Android. @@ -252,8 +254,8 @@ class IOSPush { return Future.value(); } - return await _module.channel.invokeMethod( - 'push#ios#setAutobadgeEnabled', enabled); + return await _module.channel + .invokeMethod('push#ios#setAutobadgeEnabled', enabled); } /// Sets the [badge] number on iOS. Badging is not supported for Android. @@ -261,8 +263,7 @@ class IOSPush { if (!Platform.isIOS) { return Future.value(); } - return await _module.channel.invokeMethod( - 'push#ios#setBadgeNumber', badge); + return await _module.channel.invokeMethod('push#ios#setBadgeNumber', badge); } /// Gets the [badge] number on iOS. Badging is not supported for Android. @@ -282,46 +283,43 @@ class IOSPush { } /// Gets the authorized notification settings. - Future> get authorizedNotificationSettings async { + Future> + get authorizedNotificationSettings async { if (!Platform.isIOS) { return Future.value(List.empty()); } - var strings = List.from( - await _module.channel.invokeMethod( - 'push#ios#getAuthorizedNotificationSettings') - ); + var strings = List.from(await _module.channel + .invokeMethod('push#ios#getAuthorizedNotificationSettings')); return AirshipUtils.parseIOSAuthorizedSettings(strings); } /// Gets the authorized notification status. - Future< - IOSAuthorizedNotificationStatus> get authorizedNotificationStatus async { + Future + get authorizedNotificationStatus async { if (!Platform.isIOS) { return Future.value(IOSAuthorizedNotificationStatus.notDetermined); } - var status = await _module.channel.invokeMethod( - 'push#ios#getAuthorizedNotificationStatus'); + var status = await _module.channel + .invokeMethod('push#ios#getAuthorizedNotificationStatus'); return AirshipUtils.parseIOSAuthorizedStatus(status); } /// Gets the authorized settings changed event stream. - Stream< - IOSAuthorizedNotificationSettingsChangedEvent> get onAuthorizedSettingsChanged { - + Stream + get onAuthorizedSettingsChanged { if (!Platform.isIOS) { return Stream.empty(); } return _module .getEventStream( - "com.airship.flutter/event/ios_authorized_notification_settings_changed") + "com.airship.flutter/event/ios_authorized_notification_settings_changed") .map((dynamic value) => - IOSAuthorizedNotificationSettingsChangedEvent.fromJson(value)); + IOSAuthorizedNotificationSettingsChangedEvent.fromJson(value)); } } -typedef AndroidBackgroundPushReceivedHandler = Future< - void> Function(PushReceivedEvent pushReceivedEvent); +typedef AndroidBackgroundPushReceivedHandler = Future Function( + PushReceivedEvent pushReceivedEvent); diff --git a/lib/src/live_activity.dart b/lib/src/live_activity.dart new file mode 100644 index 00000000..f011f5f9 --- /dev/null +++ b/lib/src/live_activity.dart @@ -0,0 +1,187 @@ +/// Live Activity info. +class LiveActivity { + final String id; + final String attributeTypes; + final LiveActivityContent content; + final Map attributes; + final String state; + + LiveActivity({ + required this.id, + required this.attributeTypes, + required this.content, + required this.attributes, + required this.state, + }); + + static Map _ensureStringDynamicMap(dynamic data) { + if (data is Map) { + return data; + } else if (data is Map) { + return data.map((key, value) => MapEntry(key.toString(), + value is Map ? _ensureStringDynamicMap(value) : value)); + } + throw FormatException( + 'Invalid data format: expected a Map, got ${data.runtimeType}'); + } + + factory LiveActivity.fromJson(dynamic json) { + final Map data = _ensureStringDynamicMap(json); + + return LiveActivity( + id: data['id'] as String? ?? '', + attributeTypes: data['attributeTypes'] as String? ?? '', + content: LiveActivityContent.fromJson(data['content']), + attributes: _ensureStringDynamicMap(data['attributes'] ?? {}), + state: data['state'] as String? ?? '', + ); + } + + Map toJson() => { + 'id': id, + 'attributeTypes': attributeTypes, + 'content': content.toJson(), + 'attributes': attributes, + 'state': state, + }; +} + +class LiveActivityContent { + final Map state; + final String? staleDate; + final double relevanceScore; + + LiveActivityContent({ + required this.state, + this.staleDate, + required this.relevanceScore, + }); + + factory LiveActivityContent.fromJson(dynamic json) { + final Map data = + LiveActivity._ensureStringDynamicMap(json); + + return LiveActivityContent( + state: LiveActivity._ensureStringDynamicMap(data['state'] ?? {}), + staleDate: data['staleDate'] as String?, + relevanceScore: (data['relevanceScore'] as num?)?.toDouble() ?? 0.0, + ); + } + + Map toJson() => { + 'state': state, + 'staleDate': staleDate, + 'relevanceScore': relevanceScore, + }; +} + +/// Base Live Activity request. +abstract class LiveActivityRequest { + final String attributesType; + + const LiveActivityRequest({required this.attributesType}); + + Map toJson(); +} + +/// Live Activity list request. +class LiveActivityListRequest extends LiveActivityRequest { + const LiveActivityListRequest({required String attributesType}) + : super(attributesType: attributesType); + + @override + Map toJson() => {'attributesType': attributesType}; +} + +/// Live Activity start request. +class LiveActivityStartRequest extends LiveActivityRequest { + final LiveActivityContent content; + final Map attributes; + + const LiveActivityStartRequest({ + required String attributesType, + required this.content, + required this.attributes, + }) : super(attributesType: attributesType); + + @override + Map toJson() => { + 'attributesType': attributesType, + 'content': content.toJson(), + 'attributes': attributes, + }; +} + +/// Live Activity update request. +class LiveActivityUpdateRequest extends LiveActivityRequest { + final String activityId; + final LiveActivityContent content; + + const LiveActivityUpdateRequest({ + required String attributesType, + required this.activityId, + required this.content, + }) : super(attributesType: attributesType); + + @override + Map toJson() => { + 'attributesType': attributesType, + 'activityId': activityId, + 'content': content.toJson(), + }; +} + +/// Live Activity end request. +class LiveActivityStopRequest extends LiveActivityRequest { + final String activityId; + final LiveActivityContent? content; + final LiveActivityDismissalPolicy dismissalPolicy; + + const LiveActivityStopRequest({ + required String attributesType, + required this.activityId, + this.content, + this.dismissalPolicy = const LiveActivityDismissalPolicyDefault(), + }) : super(attributesType: attributesType); + + @override + Map toJson() => { + 'attributesType': attributesType, + 'activityId': activityId, + if (content != null) 'content': content!.toJson(), + 'dismissalPolicy': dismissalPolicy.toJson(), + }; +} + +/// Live Activity dismissal policy. +abstract class LiveActivityDismissalPolicy { + const LiveActivityDismissalPolicy(); + + Map toJson(); +} + +class LiveActivityDismissalPolicyImmediate extends LiveActivityDismissalPolicy { + const LiveActivityDismissalPolicyImmediate(); + + @override + Map toJson() => const {'type': 'immediate'}; +} + +class LiveActivityDismissalPolicyDefault extends LiveActivityDismissalPolicy { + const LiveActivityDismissalPolicyDefault(); + + @override + Map toJson() => const {'type': 'default'}; +} + +class LiveActivityDismissalPolicyAfterDate extends LiveActivityDismissalPolicy { + final String date; + + const LiveActivityDismissalPolicyAfterDate(this.date); + + @override + Map toJson() => { + 'type': 'after', + 'date': date, + }; +} diff --git a/lib/src/live_update.dart b/lib/src/live_update.dart new file mode 100644 index 00000000..cd168134 --- /dev/null +++ b/lib/src/live_update.dart @@ -0,0 +1,118 @@ +/// Live Update info. +class LiveUpdate { + final String name; + final String type; + final dynamic content; + final String lastContentUpdateTimestamp; + final String lastStateChangeTimestamp; + final String? dismissTimestamp; + + LiveUpdate({ + required this.name, + required this.type, + required this.content, + required this.lastContentUpdateTimestamp, + required this.lastStateChangeTimestamp, + this.dismissTimestamp, + }); + + factory LiveUpdate.fromJson(dynamic json) { + return LiveUpdate( + name: json['name'] as String, + type: json['type'] as String, + content: json['content'] as dynamic, + lastContentUpdateTimestamp: json['lastContentUpdateTimestamp'] as String, + lastStateChangeTimestamp: json['lastStateChangeTimestamp'] as String, + dismissTimestamp: json['dismissTimestamp'] as String?, + ); + } + + Map toJson() => { + 'name': name, + 'type': type, + 'content': content, + 'lastContentUpdateTimestamp': lastContentUpdateTimestamp, + 'lastStateChangeTimestamp': lastStateChangeTimestamp, + 'dismissTimestamp': dismissTimestamp, + }; +} + +/// Live Update list request. +class LiveUpdateListRequest { + final String type; + + const LiveUpdateListRequest({required this.type}); + + Map toJson() => {'type': type}; +} + +/// Live Update update request. +class LiveUpdateUpdateRequest { + final String name; + final Map content; + final String? timestamp; + final String? dismissTimestamp; + + const LiveUpdateUpdateRequest({ + required this.name, + required this.content, + this.timestamp, + this.dismissTimestamp, + }); + + Map toJson() => { + 'name': name, + 'content': content, + if (timestamp != null) 'timestamp': timestamp, + if (dismissTimestamp != null) 'dismissTimestamp': dismissTimestamp, + }; +} + +/// Live Update end request. +class LiveUpdateEndRequest { + final String name; + final Map? content; + final String? timestamp; + final String? dismissTimestamp; + + const LiveUpdateEndRequest({ + required this.name, + this.content, + this.timestamp, + this.dismissTimestamp, + }); + + Map toJson() => { + 'name': name, + if (content != null) 'content': content, + if (timestamp != null) 'timestamp': timestamp, + if (dismissTimestamp != null) 'dismissTimestamp': dismissTimestamp, + }; +} + +/// Live Update start request. +class LiveUpdateStartRequest { + final String name; + final String type; + final Map content; + final String? timestamp; + final String? dismissalTimestamp; + + const LiveUpdateStartRequest({ + required this.name, + required this.type, + required this.content, + this.timestamp, + this.dismissalTimestamp, + }); + + Map toJson() { + return { + 'name': name, + 'type': type, + 'content': content, + if (timestamp != null) 'timestamp': timestamp, + if (dismissalTimestamp != null) 'dismissalTimestamp': dismissalTimestamp, + }; + } +} diff --git a/lib/src/push_notification_status.dart b/lib/src/push_notification_status.dart index 0c9e224a..92bd2299 100644 --- a/lib/src/push_notification_status.dart +++ b/lib/src/push_notification_status.dart @@ -1,7 +1,5 @@ - /// Push notification status object. class PushNotificationStatus { - /// If user notifications are enabled. final bool isUserNotificationsEnabled; @@ -21,26 +19,85 @@ class PushNotificationStatus { /// is true but `isOptedIn` is false, that means push token was not able to be registered. final bool isUserOptedIn; + /// The notification permission status. + final PermissionStatus notificationPermissionStatus; + const PushNotificationStatus._internal( - this.isUserNotificationsEnabled, this.areNotificationsAllowed, this.isPushPrivacyFeatureEnabled, - this.isPushTokenRegistered, this.isOptedIn, this.isUserOptedIn); + this.isUserNotificationsEnabled, + this.areNotificationsAllowed, + this.isPushPrivacyFeatureEnabled, + this.isPushTokenRegistered, + this.isOptedIn, + this.isUserOptedIn, + this.notificationPermissionStatus); static PushNotificationStatus fromJson(dynamic json) { - var isUserNotificationsEnabled = json["isUserNotificationsEnabled"] ?? false; + var isUserNotificationsEnabled = + json["isUserNotificationsEnabled"] ?? false; var areNotificationsAllowed = json["areNotificationsAllowed"] ?? false; - var isPushPrivacyFeatureEnabled = json["isPushPrivacyFeatureEnabled"] ?? false; + var isPushPrivacyFeatureEnabled = + json["isPushPrivacyFeatureEnabled"] ?? false; var isPushTokenRegistered = json["isPushTokenRegistered"] ?? false; var isOptedIn = json["isOptedIn"] ?? false; var isUserOptedIn = json["isUserOptedIn"] ?? false; + var notificationPermissionStatus = + json["notificationPermissionStatus"] ?? PermissionStatus.notDetermined; return PushNotificationStatus._internal( - isUserNotificationsEnabled, areNotificationsAllowed, isPushPrivacyFeatureEnabled, - isPushTokenRegistered, isOptedIn, isUserOptedIn); + isUserNotificationsEnabled, + areNotificationsAllowed, + isPushPrivacyFeatureEnabled, + isPushTokenRegistered, + isOptedIn, + isUserOptedIn, + notificationPermissionStatus); } @override String toString() { return "PushNotificationStatus(isUserNotificationsEnabled=$isUserNotificationsEnabled," " areNotificationsAllowed=$areNotificationsAllowed, isPushPrivacyFeatureEnabled=$isPushPrivacyFeatureEnabled," - " isPushTokenRegistered=$isPushTokenRegistered, isOptedIn=$isOptedIn, isUserOptedIn=$isUserOptedIn)"; + " isPushTokenRegistered=$isPushTokenRegistered, isOptedIn=$isOptedIn, isUserOptedIn=$isUserOptedIn, notificationPermissionStatus=$notificationPermissionStatus)"; + } +} + +/// Enum of permission status. +enum PermissionStatus { + /// Permission is granted. + granted, + + /// Permission is denied. + denied, + + /// Permission has not yet been requested. + notDetermined, +} + +/// Fallback when prompting for permission and the permission is +/// already denied on iOS or is denied silently on Android. +enum PromptPermissionFallback { + // Take the user to the system settings to enable the permission. + systemSettings, +} + +/// Options for enabling push notifications. +class EnableUserPushNotificationsArgs { + /// Optional fallback strategy. + final PromptPermissionFallback? fallback; + + /// Creates a new instance of [EnableUserPushNotificationsArgs]. + /// + /// Both [fallback] and [options] are optional. + EnableUserPushNotificationsArgs({ + this.fallback, + }); + + Map toJson() { + final Map json = {}; + + if (fallback != null) { + json['fallback'] = fallback!.name; + } + + return json; } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 629521c7..218cc609 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: airship_flutter description: "Cross-platform plugin interface for the native Airship iOS and Android SDKs. Simplifies adding Airship to Flutter apps." -version: 7.8.2 +version: 7.9.0 homepage: https://www.airship.com/ repository: https://github.com/urbanairship/airship-flutter issue_tracker: https://github.com/urbanairship/airship-flutter/issues