From d320112fd54c17fd78bafc519becdc854d531b7e Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Tue, 27 Jan 2026 15:44:18 -0500 Subject: [PATCH] feat: push notifications --- .../workflows/android-internal-release.yml | 3 + .gitignore | 4 + android/app/build.gradle.kts | 3 + android/settings.gradle.kts | 3 + ios/Runner.xcodeproj/project.pbxproj | 24 ++- .../xcshareddata/swiftpm/Package.resolved | 140 +++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 140 +++++++++++++++ ios/ci_scripts/ci_post_clone.sh | 8 + lib/firebase_options.dart | 68 +++++++ .../components/molecules/feed_tag_list.dart | 2 - lib/src/core/di/service_locator.dart | 11 ++ .../repositories/notification_repository.dart | 22 +++ .../notification_repository_impl.dart | 86 +++++++++ .../push_notification_service.dart | 167 ++++++++++++++++++ .../auth/providers/auth_providers.dart | 160 +++++++++++++++++ .../feed/ui/widgets/feed/feeds_bar.dart | 18 +- lib/src/features/home/ui/pages/main_page.dart | 12 ++ .../providers/notification_provider.dart | 4 +- .../ui/pages/notifications_page.dart | 29 ++- pubspec.lock | 64 +++++++ pubspec.yaml | 3 + .../Flutter/GeneratedPluginRegistrant.swift | 6 + 22 files changed, 939 insertions(+), 38 deletions(-) create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 lib/firebase_options.dart create mode 100644 lib/src/core/notifications/push_notification_service.dart diff --git a/.github/workflows/android-internal-release.yml b/.github/workflows/android-internal-release.yml index 9b19e03e..ba7dc371 100644 --- a/.github/workflows/android-internal-release.yml +++ b/.github/workflows/android-internal-release.yml @@ -43,6 +43,9 @@ jobs: flutter pub get --enforce-lockfile dart run build_runner build --delete-conflicting-outputs + - name: Setup Firebase config + run: echo "${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}" | base64 -d > android/app/google-services.json + - name: Setup keys uses: timheuer/base64-to-file@v1 with: diff --git a/.gitignore b/.gitignore index dc6f2440..deadea8d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,10 @@ app.*.map.json # Environment variables .env +# Firebase config files (stored as secrets in CI) +android/app/google-services.json +ios/Runner/GoogleService-Info.plist + .vscode/ # Generated files diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 20bc4e42..86151446 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -4,6 +4,9 @@ import java.io.FileInputStream plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") id("org.jetbrains.kotlin.plugin.compose") id("dev.flutter.flutter-gradle-plugin") diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index a57edd6d..1d8a2e55 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,6 +19,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.13.2" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" apply false id("com.google.devtools.ksp") version "2.2.21-2.0.4" apply false diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 791d26aa..7cf96e6e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,11 +13,12 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 59C723F32EA9C6BF002F18BF /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 59C723F22EA9C6BF002F18BF /* AppIcon.icon */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 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 */; }; + 9B6EB3EAD1043C920A48987A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */; }; C788453B52B48F95B80D3F19 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E62B4B048ACF350C4CFD2DF /* Pods_RunnerTests.framework */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,8 +58,10 @@ 4E62B4B048ACF350C4CFD2DF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 59C723F22EA9C6BF002F18BF /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 59DA3B292DA45BAD00C44421 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 86577EB0EB28471AE1014B20 /* 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -69,7 +72,6 @@ 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 = ""; }; E84053FC439008C2EBB39FE0 /* 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 = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -123,6 +125,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */, BA71D3C7868C6E4326FA2D8D /* Pods */, A72872E4BD53F060F12B36CF /* Frameworks */, + 622B2DC1E9685418514D2FC4 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -196,9 +199,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -216,6 +216,9 @@ dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -224,9 +227,6 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -252,6 +252,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -279,6 +282,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 9B6EB3EAD1043C920A48987A /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -756,12 +760,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..5125d839 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,140 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", + "version" : "11.15.0" + } + }, + { + "identity" : "flutterfire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/flutterfire", + "state" : { + "revision" : "dadb0fd27bc9afe4dee4f23326a4a9ba238258ac", + "version" : "3.15.2-firebase-core-swift" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", + "version" : "2.3.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", + "version" : "11.15.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", + "version" : "4.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" + } + } + ], + "version" : 2 +} diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..5125d839 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,140 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", + "version" : "11.15.0" + } + }, + { + "identity" : "flutterfire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/flutterfire", + "state" : { + "revision" : "dadb0fd27bc9afe4dee4f23326a4a9ba238258ac", + "version" : "3.15.2-firebase-core-swift" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "a2d0f1f1666de591eb1a811f40b1706f5c63a2ed", + "version" : "2.3.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", + "version" : "11.15.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", + "version" : "4.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", + "version" : "1.33.3" + } + } + ], + "version" : 2 +} diff --git a/ios/ci_scripts/ci_post_clone.sh b/ios/ci_scripts/ci_post_clone.sh index 0e6e9afe..61b55c0f 100755 --- a/ios/ci_scripts/ci_post_clone.sh +++ b/ios/ci_scripts/ci_post_clone.sh @@ -24,6 +24,14 @@ MESSAGES_SERVICE_URL=https://chat.sprk.so SHOWCASES_LICENSE_FLUTTER=$SHOWCASES_LICENSE_FLUTTER EOL +# Decode Firebase config from base64 environment variable +if [ -n "$GOOGLE_SERVICE_INFO_PLIST_BASE64" ]; then + echo "Decoding GoogleService-Info.plist..." + echo "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 -d > ios/Runner/GoogleService-Info.plist +else + echo "Warning: GOOGLE_SERVICE_INFO_PLIST_BASE64 not set" +fi + # Install CocoaPods using Homebrew. HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates. brew install cocoapods diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..3c3d71ed --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCZUyZLcOZkt5FohWAIdDIA4ubnTkEJtKI', + appId: '1:244807562864:android:04ab7d3cda19185e2e66b5', + messagingSenderId: '244807562864', + projectId: 'sprk-social', + storageBucket: 'sprk-social.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBYBZLhGhUkObNbwLCXO1PmtPuEa8zthdI', + appId: '1:244807562864:ios:e5004433a6cff5ab2e66b5', + messagingSenderId: '244807562864', + projectId: 'sprk-social', + storageBucket: 'sprk-social.firebasestorage.app', + iosBundleId: 'so.sprk.app', + ); +} diff --git a/lib/src/core/design_system/components/molecules/feed_tag_list.dart b/lib/src/core/design_system/components/molecules/feed_tag_list.dart index 89a8623f..d6e95d68 100644 --- a/lib/src/core/design_system/components/molecules/feed_tag_list.dart +++ b/lib/src/core/design_system/components/molecules/feed_tag_list.dart @@ -204,8 +204,6 @@ class _FeedTagListState extends State { 1.0, ); return LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, colors: const [ Colors.white, Colors.white, diff --git a/lib/src/core/di/service_locator.dart b/lib/src/core/di/service_locator.dart index edaf8aba..700c7a05 100644 --- a/lib/src/core/di/service_locator.dart +++ b/lib/src/core/di/service_locator.dart @@ -5,6 +5,8 @@ import 'package:spark/src/core/auth/data/repositories/onboarding_repository_impl import 'package:spark/src/core/network/atproto/atproto.dart'; import 'package:spark/src/core/network/atproto/data/repositories/actor_repository_impl.dart'; import 'package:spark/src/core/network/atproto/data/repositories/graph_repository_impl.dart'; +import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; +import 'package:spark/src/core/network/atproto/data/repositories/notification_repository_impl.dart'; import 'package:spark/src/core/network/atproto/data/repositories/pref_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/pref_repository_impl.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sound_repository.dart'; @@ -14,6 +16,7 @@ import 'package:spark/src/core/network/atproto/data/repositories/story_repositor import 'package:spark/src/core/network/messages/data/repository/messages_repository.dart'; import 'package:spark/src/core/network/messages/data/repository/messages_repository_xrpc.dart'; import 'package:spark/src/core/network/xrpc/service_auth_helper.dart'; +import 'package:spark/src/core/notifications/push_notification_service.dart'; import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository.dart'; import 'package:spark/src/core/pro_video_editor/pro_video_editor_repository_impl.dart'; import 'package:spark/src/core/storage/cache/download_manager_interface.dart'; @@ -87,5 +90,13 @@ Future initServiceLocator() async { ) ..registerSingleton( const ProVideoEditorRepositoryImpl(), + ) + ..registerSingleton( + NotificationRepositoryImpl(sl()), ); + + // Initialize push notification service asynchronously + final pushService = PushNotificationService(); + await pushService.initialize(); + sl.registerSingleton(pushService); } diff --git a/lib/src/core/network/atproto/data/repositories/notification_repository.dart b/lib/src/core/network/atproto/data/repositories/notification_repository.dart index 4df56ac4..de79f89e 100644 --- a/lib/src/core/network/atproto/data/repositories/notification_repository.dart +++ b/lib/src/core/network/atproto/data/repositories/notification_repository.dart @@ -24,4 +24,26 @@ abstract class NotificationRepository { /// /// [seenAt] The timestamp to mark notifications as seen at Future updateSeen(DateTime seenAt); + + /// Register device for push notifications + /// + /// [token] The FCM/APNs device token + /// [platform] The platform identifier ('ios' or 'android') + /// [appId] The application identifier (e.g., 'so.sprk.app') + Future registerPush({ + required String token, + required String platform, + required String appId, + }); + + /// Unregister device from push notifications + /// + /// [token] The FCM/APNs device token to unregister + /// [platform] The platform identifier ('ios' or 'android') + /// [appId] The application identifier (e.g., 'so.sprk.app') + Future unregisterPush({ + required String token, + required String platform, + required String appId, + }); } diff --git a/lib/src/core/network/atproto/data/repositories/notification_repository_impl.dart b/lib/src/core/network/atproto/data/repositories/notification_repository_impl.dart index b52d284e..175b1ad3 100644 --- a/lib/src/core/network/atproto/data/repositories/notification_repository_impl.dart +++ b/lib/src/core/network/atproto/data/repositories/notification_repository_impl.dart @@ -5,6 +5,7 @@ import 'package:get_it/get_it.dart'; import 'package:spark/src/core/network/atproto/data/models/notification_models.dart'; import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; +import 'package:spark/src/core/notifications/push_notification_service.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; @@ -128,7 +129,92 @@ class NotificationRepositoryImpl implements NotificationRepository { headers: {'atproto-proxy': _client.sprkDid}, ); + // Clear the app badge locally (server also sends silent push for background) + try { + await GetIt.instance().clearBadge(); + } catch (e) { + _logger.w('Failed to clear badge: $e'); + } + _logger.d('Seen timestamp updated successfully'); }); } + + @override + Future registerPush({ + required String token, + required String platform, + required String appId, + }) async { + _logger.d('Registering push token: platform=$platform, appId=$appId'); + return _client.executeWithRetry(() async { + if (!_client.authRepository.isAuthenticated) { + _logger.w('Not authenticated'); + throw Exception('Not authenticated'); + } + + final atproto = _client.authRepository.atproto; + if (atproto == null) { + _logger.e('AtProto not initialized'); + throw Exception('AtProto not initialized'); + } + + // serviceDid needs just the DID without fragment (format validation) + final serviceDid = _client.sprkDid.split('#').first; + + final body = { + 'serviceDid': serviceDid, + 'token': token, + 'platform': platform, + 'appId': appId, + }; + + await atproto.post( + NSID.parse('so.sprk.notification.registerPush'), + body: body, + headers: {'atproto-proxy': _client.sprkDid}, + ); + + _logger.i('Push token registered successfully'); + }); + } + + @override + Future unregisterPush({ + required String token, + required String platform, + required String appId, + }) async { + _logger.d('Unregistering push token: platform=$platform, appId=$appId'); + return _client.executeWithRetry(() async { + if (!_client.authRepository.isAuthenticated) { + _logger.w('Not authenticated'); + throw Exception('Not authenticated'); + } + + final atproto = _client.authRepository.atproto; + if (atproto == null) { + _logger.e('AtProto not initialized'); + throw Exception('AtProto not initialized'); + } + + // serviceDid needs just the DID without fragment (format validation) + final serviceDid = _client.sprkDid.split('#').first; + + final body = { + 'serviceDid': serviceDid, + 'token': token, + 'platform': platform, + 'appId': appId, + }; + + await atproto.post( + NSID.parse('so.sprk.notification.unregisterPush'), + body: body, + headers: {'atproto-proxy': _client.sprkDid}, + ); + + _logger.i('Push token unregistered successfully'); + }); + } } diff --git a/lib/src/core/notifications/push_notification_service.dart b/lib/src/core/notifications/push_notification_service.dart new file mode 100644 index 00000000..3fae8e8d --- /dev/null +++ b/lib/src/core/notifications/push_notification_service.dart @@ -0,0 +1,167 @@ +import 'dart:io'; + +import 'package:app_badge_plus/app_badge_plus.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:get_it/get_it.dart'; +import 'package:spark/src/core/utils/logging/log_service.dart'; +import 'package:spark/src/core/utils/logging/logger.dart'; + +/// Service for managing push notifications via Firebase Cloud Messaging +class PushNotificationService { + PushNotificationService() { + _logger.v('PushNotificationService initialized'); + } + + late final FirebaseMessaging _messaging; + final SparkLogger _logger = GetIt.instance().getLogger( + 'PushNotificationService', + ); + + String? _currentToken; + bool _badgeSupported = false; + bool _initialized = false; + + /// Initializes Firebase without requesting permissions + /// Permissions should be requested separately via [requestPermissionAndGetToken] + Future initialize() async { + _logger.i('Initializing push notification service'); + + try { + await Firebase.initializeApp(); + _messaging = FirebaseMessaging.instance; + _logger.d('Firebase initialized'); + + // Listen for token refresh + _messaging.onTokenRefresh.listen(_onTokenRefresh); + + // Check if badge is supported on this device + _badgeSupported = await AppBadgePlus.isSupported(); + _logger.d('Badge support: $_badgeSupported'); + + _initialized = true; + _logger.i('Push notification service initialized successfully'); + } catch (e, stackTrace) { + _logger.e( + 'Failed to initialize push notifications', + error: e, + stackTrace: stackTrace, + ); + } + } + + /// Returns true if notification permissions are already granted + Future hasPermission() async { + if (!_initialized) return false; + + try { + final settings = await _messaging.getNotificationSettings(); + return settings.authorizationStatus == AuthorizationStatus.authorized || + settings.authorizationStatus == AuthorizationStatus.provisional; + } catch (e) { + _logger.e('Failed to check permission status', error: e); + return false; + } + } + + /// Requests notification permissions from the user + /// Returns true if permission was granted + Future requestPermission() async { + if (!_initialized) return false; + + _logger.d('Requesting notification permissions'); + + try { + final settings = await _messaging.requestPermission(); + + if (settings.authorizationStatus == AuthorizationStatus.authorized) { + _logger.i('User granted notification permission'); + return true; + } else if (settings.authorizationStatus == + AuthorizationStatus.provisional) { + _logger.i('User granted provisional notification permission'); + return true; + } else { + _logger.w('User denied notification permission'); + return false; + } + } catch (e, stackTrace) { + _logger.e( + 'Failed to request permission', + error: e, + stackTrace: stackTrace, + ); + return false; + } + } + + /// Requests permission (if needed) and returns the FCM token + /// Returns null if permission denied or error occurs + Future requestPermissionAndGetToken() async { + if (!_initialized) return null; + + final hasExistingPermission = await hasPermission(); + + if (!hasExistingPermission) { + final granted = await requestPermission(); + if (!granted) return null; + } + + return getToken(); + } + + /// Handles FCM token refresh + void _onTokenRefresh(String token) { + _logger.d('FCM token refreshed: ${token.substring(0, 10)}...'); + _currentToken = token; + // Token refresh registration is handled by the auth flow + // which will call registerPush with the new token + } + + /// Returns the current FCM token, or null if not available + /// Note: This requires permission to be granted first + Future getToken() async { + if (!_initialized) return null; + + try { + _currentToken ??= await _messaging.getToken(); + if (_currentToken != null) { + _logger.d('FCM token obtained: ${_currentToken!.substring(0, 10)}...'); + } + return _currentToken; + } catch (e) { + _logger.e('Failed to get FCM token', error: e); + return null; + } + } + + /// Returns the platform identifier ('ios' or 'android') + String get platform => Platform.isIOS ? 'ios' : 'android'; + + /// Callback for when token is refreshed - allows external registration + Stream get onTokenRefresh => _messaging.onTokenRefresh; + + /// Clears the app badge count (iOS only, no-op on Android) + Future clearBadge() async { + if (!_badgeSupported) return; + + try { + await AppBadgePlus.updateBadge(0); + _logger.d('Badge cleared'); + } catch (e, stackTrace) { + _logger.e('Failed to clear badge', error: e, stackTrace: stackTrace); + } + } + + /// Updates the app badge count (iOS only, no-op on Android) + Future updateBadge(int count) async { + if (!_badgeSupported) return; + + try { + await AppBadgePlus.updateBadge(count); + _logger.d('Badge updated to $count'); + } catch (e, stackTrace) { + _logger.e('Failed to update badge', error: e, stackTrace: stackTrace); + } + } +} diff --git a/lib/src/features/auth/providers/auth_providers.dart b/lib/src/features/auth/providers/auth_providers.dart index c16affe4..a33ba09c 100644 --- a/lib/src/features/auth/providers/auth_providers.dart +++ b/lib/src/features/auth/providers/auth_providers.dart @@ -1,8 +1,12 @@ +import 'dart:async'; + import 'package:atproto/atproto.dart'; import 'package:get_it/get_it.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:spark/src/core/auth/data/models/login_result.dart'; import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; +import 'package:spark/src/core/network/atproto/data/repositories/notification_repository.dart'; +import 'package:spark/src/core/notifications/push_notification_service.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; import 'package:spark/src/features/auth/providers/auth_state.dart'; @@ -21,6 +25,8 @@ AuthRepository authRepository(Ref ref) { class Auth extends _$Auth { late final AuthRepository _authRepository; late final LogService _logService; + StreamSubscription? _tokenRefreshSubscription; + bool _pendingPushRegistration = false; @override AuthState build() { @@ -133,6 +139,9 @@ class Auth extends _$Auth { } else { _updateState(); state = state.copyWith(isLoading: false); + + // Register for push notifications after successful login + await _registerPushNotifications(); } return result; @@ -152,6 +161,9 @@ class Auth extends _$Auth { state = state.copyWith(isLoading: true, error: null); try { + // Unregister push notifications before logout + await _unregisterPushNotifications(); + await _authRepository.logout(); _updateState(); state = state.copyWith(isLoading: false); @@ -192,6 +204,154 @@ class Auth extends _$Auth { return false; } } + + /// Registers the device for push notifications + /// Only registers if permission is already granted, otherwise defers + Future _registerPushNotifications() async { + try { + final pushService = GetIt.instance(); + + // Check if we already have permission + final hasPermission = await pushService.hasPermission(); + + if (hasPermission) { + // Permission already granted, register immediately + await _doRegisterPush(pushService); + } else { + // Permission not granted yet, defer until main screen + _pendingPushRegistration = true; + _logger.i( + 'Push permission not granted, deferring registration to main screen', + ); + } + } catch (e, stackTrace) { + // Don't fail login if push registration fails + _logger.e( + 'Failed to register push notifications', + error: e, + stackTrace: stackTrace, + ); + } + } + + /// Actually performs push registration (called when we have permission) + Future _doRegisterPush(PushNotificationService pushService) async { + final token = await pushService.getToken(); + + if (token != null) { + final notificationRepo = GetIt.instance(); + await notificationRepo.registerPush( + token: token, + platform: pushService.platform, + appId: 'so.sprk.app', + ); + _logger.i('Push notifications registered successfully'); + + // Set up listener for token refresh + await _setupTokenRefreshListener(pushService, notificationRepo); + _pendingPushRegistration = false; + } else { + _logger.w('No push token available'); + } + } + + /// Returns true if push registration is pending (permission not yet requested) + bool get hasPendingPushRegistration => _pendingPushRegistration; + + /// Requests push notification permission and registers if granted + /// Call this from the main screen after login + Future requestPushPermissionAndRegister() async { + if (!_pendingPushRegistration) { + _logger.d('No pending push registration'); + return true; + } + + try { + final pushService = GetIt.instance(); + final granted = await pushService.requestPermission(); + + if (granted) { + await _doRegisterPush(pushService); + return true; + } else { + _logger.w('User denied push notification permission'); + _pendingPushRegistration = false; + return false; + } + } catch (e, stackTrace) { + _logger.e( + 'Failed to request push permission', + error: e, + stackTrace: stackTrace, + ); + return false; + } + } + + /// Sets up a listener for FCM token refresh to re-register + Future _setupTokenRefreshListener( + PushNotificationService pushService, + NotificationRepository notificationRepo, + ) async { + // Cancel any existing subscription and wait for it to complete + await _tokenRefreshSubscription?.cancel(); + + _tokenRefreshSubscription = pushService.onTokenRefresh.listen( + (newToken) async { + _logger.i('FCM token refreshed, re-registering push notifications'); + try { + await notificationRepo.registerPush( + token: newToken, + platform: pushService.platform, + appId: 'so.sprk.app', + ); + _logger.i('Push notifications re-registered with new token'); + } catch (e, stackTrace) { + _logger.e( + 'Failed to re-register push notifications after token refresh', + error: e, + stackTrace: stackTrace, + ); + } + }, + onError: (Object error, StackTrace stackTrace) { + _logger.e( + 'Error in token refresh stream', + error: error, + stackTrace: stackTrace, + ); + }, + ); + } + + /// Unregisters the device from push notifications + Future _unregisterPushNotifications() async { + // Cancel token refresh listener + await _tokenRefreshSubscription?.cancel(); + _tokenRefreshSubscription = null; + + try { + final pushService = GetIt.instance(); + final token = await pushService.getToken(); + + if (token != null) { + final notificationRepo = GetIt.instance(); + await notificationRepo.unregisterPush( + token: token, + platform: pushService.platform, + appId: 'so.sprk.app', + ); + _logger.i('Push notifications unregistered successfully'); + } + } catch (e, stackTrace) { + // Don't fail logout if push unregistration fails + _logger.e( + 'Failed to unregister push notifications', + error: e, + stackTrace: stackTrace, + ); + } + } } /// Convenience provider for checking if the user is authenticated diff --git a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart index 7d7cf5d0..ab6478ae 100644 --- a/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart +++ b/lib/src/features/feed/ui/widgets/feed/feeds_bar.dart @@ -63,10 +63,9 @@ class _FeedsBarState extends ConsumerState { height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onSurface - .withAlpha(50), + color: Theme.of( + context, + ).colorScheme.onSurface.withAlpha(50), borderRadius: BorderRadius.circular(2), ), ), @@ -79,8 +78,8 @@ class _FeedsBarState extends ConsumerState { child: Text( feed.view?.displayName ?? 'Following', style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + fontWeight: FontWeight.bold, + ), ), ), const Divider(), @@ -142,7 +141,7 @@ class _FeedsBarState extends ConsumerState { ], ), ); - if (confirmed == true) { + if (confirmed ?? false) { await ref .read(settingsProvider.notifier) .removeFeed(feed); @@ -168,8 +167,9 @@ class _FeedsBarState extends ConsumerState { final settings = ref.watch(settingsProvider); // Only show pinned feeds in the home view - final pinnedFeeds = - settings.feeds.where((feed) => feed.config.pinned).toList(); + final pinnedFeeds = settings.feeds + .where((feed) => feed.config.pinned) + .toList(); final tags = pinnedFeeds.map((feed) { final isTimeline = diff --git a/lib/src/features/home/ui/pages/main_page.dart b/lib/src/features/home/ui/pages/main_page.dart index 22d5bfcd..ffabe7d5 100644 --- a/lib/src/features/home/ui/pages/main_page.dart +++ b/lib/src/features/home/ui/pages/main_page.dart @@ -26,6 +26,18 @@ class _MainPageState extends ConsumerState { @override void initState() { super.initState(); + // Request push notification permission if pending (after login) + WidgetsBinding.instance.addPostFrameCallback((_) { + _requestPushPermissionIfNeeded(); + }); + } + + /// Requests push notification permission if it was deferred during login + Future _requestPushPermissionIfNeeded() async { + final auth = ref.read(authProvider.notifier); + if (auth.hasPendingPushRegistration) { + await auth.requestPushPermissionAndRegister(); + } } void _updateSystemUIOverlayStyle(int activeIndex, BuildContext context) { diff --git a/lib/src/features/notifications/providers/notification_provider.dart b/lib/src/features/notifications/providers/notification_provider.dart index fb334661..fa31e2e0 100644 --- a/lib/src/features/notifications/providers/notification_provider.dart +++ b/lib/src/features/notifications/providers/notification_provider.dart @@ -150,7 +150,7 @@ class NotificationNotifier extends _$NotificationNotifier { final mostRecent = state.notifications.first; await _notificationRepository.updateSeen(mostRecent.indexedAt); // Refresh the unread count - ref.invalidate(unreadCountProvider); + await ref.read(unreadCountProvider().notifier).refresh(); } catch (e, stackTrace) { _logger.e( 'Error marking notifications as seen: $e', @@ -174,7 +174,7 @@ class NotificationNotifier extends _$NotificationNotifier { // Use this notification's indexedAt as the seenAt timestamp await _notificationRepository.updateSeen(notification.indexedAt); // Refresh the unread count - ref.invalidate(unreadCountProvider); + await ref.read(unreadCountProvider().notifier).refresh(); } catch (e, stackTrace) { _logger.e( 'Error marking notification as viewed: $e', diff --git a/lib/src/features/notifications/ui/pages/notifications_page.dart b/lib/src/features/notifications/ui/pages/notifications_page.dart index e1325378..1ae7e204 100644 --- a/lib/src/features/notifications/ui/pages/notifications_page.dart +++ b/lib/src/features/notifications/ui/pages/notifications_page.dart @@ -4,8 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spark/src/core/ui/foundation/colors.dart'; import 'package:spark/src/features/notifications/providers/notification_provider.dart' show notificationProvider; -import 'package:spark/src/features/notifications/providers/unread_count_provider.dart' - show unreadCountProvider; import 'package:spark/src/features/notifications/ui/widgets/notifications_list.dart'; @RoutePage() @@ -17,23 +15,22 @@ class NotificationsPage extends ConsumerStatefulWidget { } class _NotificationsPageState extends ConsumerState { - @override - void initState() { - super.initState(); - // Mark notifications as seen when page is viewed - WidgetsBinding.instance.addPostFrameCallback((_) { - ref - .read( - notificationProvider().notifier, - ) - .markAsSeen(); - // Refresh unread count after marking as seen - ref.read(unreadCountProvider().notifier).refresh(); - }); - } + bool _hasMarkedAsSeen = false; @override Widget build(BuildContext context) { + final notificationState = ref.watch(notificationProvider()); + + // Mark notifications as seen once they're loaded + if (!_hasMarkedAsSeen && + !notificationState.isLoading && + notificationState.notifications.isNotEmpty) { + _hasMarkedAsSeen = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(notificationProvider().notifier).markAsSeen(); + }); + } + return Scaffold( backgroundColor: AppColors.black, appBar: AppBar( diff --git a/pubspec.lock b/pubspec.lock index 5bd19115..5ed377e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "91.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" accessibility_tools: dependency: transitive description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + app_badge_plus: + dependency: "direct main" + description: + name: app_badge_plus + sha256: "152c22866f4b6d0d4eb3c8fe6916a77bbc0e3939da11b51cce38a4a26738dd60" + url: "https://pub.dev" + source: hosted + version: "1.2.6" archive: dependency: transitive description: @@ -571,6 +587,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+5" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" + url: "https://pub.dev" + source: hosted + version: "15.2.10" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" + url: "https://pub.dev" + source: hosted + version: "4.6.10" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" + url: "https://pub.dev" + source: hosted + version: "3.10.10" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 58ddf388..be269e40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ workspace: dependencies: any_link_preview: ^3.0.3 + app_badge_plus: ^1.1.5 assets: path: assets atproto: ^1.3.0 @@ -24,6 +25,8 @@ dependencies: cached_network_image: ^3.3.1 camera: ^0.11.3 collection: ^1.19.1 + firebase_core: ^3.8.1 + firebase_messaging: ^15.1.6 fluentui_system_icons: ^1.1.273 flutter: sdk: flutter diff --git a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift index a129a556..1ec25140 100644 --- a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,9 +5,12 @@ import FlutterMacOS import Foundation +import app_badge_plus import audioplayers_darwin import desktop_webview_window import file_selector_macos +import firebase_core +import firebase_messaging import flutter_secure_storage_darwin import flutter_web_auth_2 import fvp @@ -22,9 +25,12 @@ import wakelock_plus import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppBadgePlusPlugin.register(with: registry.registrar(forPlugin: "AppBadgePlusPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin")) -- 2.51.2