diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 28da1f2081..e863e54cd8 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -37,6 +37,9 @@ + diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt index c2ad1e8472..c097094892 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/ContentView.kt @@ -41,7 +41,7 @@ import com.mbta.tid.mbta_app.android.state.getGlobalData import com.mbta.tid.mbta_app.android.state.subscribeToAlerts import com.mbta.tid.mbta_app.android.util.ManageErrorBannerBackgrounding import com.mbta.tid.mbta_app.android.util.SettingsCache -import com.mbta.tid.mbta_app.android.util.fcmToken +import com.mbta.tid.mbta_app.android.util.fcmInstallationId import com.mbta.tid.mbta_app.cache.ScheduleCache import com.mbta.tid.mbta_app.model.FeaturePromo import com.mbta.tid.mbta_app.model.OnboardingScreen @@ -105,8 +105,8 @@ fun ContentView( val pendingFeaturePromos = viewModel.pendingFeaturePromos.collectAsState().value val currentLocale = stringResource(R.string.current_locale) - LaunchedEffect(fcmToken, notificationsEnabled) { - fcmToken?.let { + LaunchedEffect(fcmInstallationId, notificationsEnabled) { + fcmInstallationId?.let { val favorites = favoritesUsecases.getRouteStopDirectionFavorites() val subscriptions = SubscriptionRequest.fromFavorites(favorites, includeAccessibility) subscriptionsRepository.updateSubscriptions( @@ -118,7 +118,9 @@ fun ContentView( } } - LaunchedEffect(fcmToken, globalResponse) { favoritesViewModel.clearStaleFavorites(fcmToken) } + LaunchedEffect(fcmInstallationId, globalResponse) { + favoritesViewModel.clearStaleFavorites(fcmInstallationId) + } val locationDataManager = rememberLocationDataManager() val mapViewportState = rememberMapViewportState { diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainActivity.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainActivity.kt index 18027cae81..843267a8cc 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainActivity.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainActivity.kt @@ -15,7 +15,6 @@ import com.google.firebase.analytics.analytics import com.google.firebase.messaging.FirebaseMessaging import com.mbta.tid.mbta_app.android.analytics.AnalyticsProvider import com.mbta.tid.mbta_app.android.util.LocalLocationClient -import com.mbta.tid.mbta_app.android.util.fcmToken import com.mbta.tid.mbta_app.initializeSentry import com.mbta.tid.mbta_app.routes.DeepLinkState import kotlinx.coroutines.flow.MutableStateFlow @@ -54,7 +53,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initSentry() - getFCMToken() + getFCMInstallationId() handleIntent(intent) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) enableEdgeToEdge() @@ -83,7 +82,7 @@ class MainActivity : ComponentActivity() { } } - private fun getFCMToken() { - FirebaseMessaging.getInstance().token.addOnSuccessListener { fcmToken = it } + private fun getFCMInstallationId() { + FirebaseMessaging.getInstance().register() } } diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/notification/MBTAGoMessagingService.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/notification/MBTAGoMessagingService.kt index 8680e79631..9fd9d213c5 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/notification/MBTAGoMessagingService.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/notification/MBTAGoMessagingService.kt @@ -14,20 +14,13 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.mbta.tid.mbta_app.android.MainActivity import com.mbta.tid.mbta_app.android.R -import com.mbta.tid.mbta_app.android.util.fcmToken +import com.mbta.tid.mbta_app.android.util.fcmInstallationId import kotlin.random.Random class MBTAGoMessagingService : FirebaseMessagingService() { - - /** - * If you want to target single devices or create device groups, you'll need this token. Because - * the token could be rotated after initial startup, it is strongly recommended to retrieve the - * latest updated registration token. If you need it any other time: - * `FirebaseMessaging.getInstance().token.addOnCompleteListener {...}` - */ - override fun onNewToken(token: String) { - super.onNewToken(token) - fcmToken = token + override fun onRegistered(installationId: String) { + super.onRegistered(installationId) + fcmInstallationId = installationId } // if a notification is sent when the app is in the foreground, we have to create the diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/EditFavoritesPage.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/EditFavoritesPage.kt index a6c21f2b4d..6bc24df6a8 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/EditFavoritesPage.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/EditFavoritesPage.kt @@ -67,7 +67,7 @@ import com.mbta.tid.mbta_app.android.favorites.NoFavoritesView import com.mbta.tid.mbta_app.android.util.IsLoadingSheetContents import com.mbta.tid.mbta_app.android.util.SettingsCache import com.mbta.tid.mbta_app.android.util.Typography -import com.mbta.tid.mbta_app.android.util.fcmToken +import com.mbta.tid.mbta_app.android.util.fcmInstallationId import com.mbta.tid.mbta_app.android.util.getLabels import com.mbta.tid.mbta_app.android.util.key import com.mbta.tid.mbta_app.android.util.modifiers.haloContainer @@ -157,7 +157,7 @@ fun EditFavoritesPage( mapOf(deletedFavorite to null), EditFavoritesContext.Favorites, deletedFavorite.direction, - fcmToken, + fcmInstallationId, currentLocale, ) @@ -173,7 +173,7 @@ fun EditFavoritesPage( mapOf(deletedFavorite to deletedSettings), EditFavoritesContext.Favorites, deletedFavorite.direction, - fcmToken, + fcmInstallationId, currentLocale, ) toastViewModel.hideToast() diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/MorePage.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/MorePage.kt index ba79ce4685..c34193d329 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/MorePage.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/MorePage.kt @@ -43,7 +43,7 @@ import com.mbta.tid.mbta_app.android.more.MoreButton import com.mbta.tid.mbta_app.android.more.MoreSectionView import com.mbta.tid.mbta_app.android.util.SettingsCache import com.mbta.tid.mbta_app.android.util.Typography -import com.mbta.tid.mbta_app.android.util.fcmToken +import com.mbta.tid.mbta_app.android.util.fcmInstallationId import com.mbta.tid.mbta_app.android.util.key import com.mbta.tid.mbta_app.android.util.modifiers.haloContainer import com.mbta.tid.mbta_app.model.Dependency @@ -133,7 +133,7 @@ fun MorePage( when (setting) { Settings.StationAccessibility -> { if (notificationsEnabled) { - fcmToken?.let { + fcmInstallationId?.let { viewModel.updateAccessibility( it, newValue, diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/SaveFavoritePage.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/SaveFavoritePage.kt index 34bf36b46e..7721942238 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/SaveFavoritePage.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/pages/SaveFavoritePage.kt @@ -48,7 +48,7 @@ import com.mbta.tid.mbta_app.android.favorites.NotificationSettingsWidget import com.mbta.tid.mbta_app.android.state.getGlobalData import com.mbta.tid.mbta_app.android.util.SettingsCache import com.mbta.tid.mbta_app.android.util.Typography -import com.mbta.tid.mbta_app.android.util.fcmToken +import com.mbta.tid.mbta_app.android.util.fcmInstallationId import com.mbta.tid.mbta_app.android.util.getLabels import com.mbta.tid.mbta_app.android.util.key import com.mbta.tid.mbta_app.android.util.notificationPermissionState @@ -153,7 +153,7 @@ fun SaveFavoritePage( update, context, selectedDirection, - fcmToken, + fcmInstallationId, currentLocale, ) } diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMInstallationId.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMInstallationId.kt new file mode 100644 index 0000000000..b47c2d32cc --- /dev/null +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMInstallationId.kt @@ -0,0 +1,8 @@ +package com.mbta.tid.mbta_app.android.util + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +/** Set in MBTAGoMessagingService or retrieved on app launch in MainActivity */ +var fcmInstallationId: String? by mutableStateOf(null) diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMToken.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMToken.kt deleted file mode 100644 index f3391c69f3..0000000000 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/FCMToken.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.mbta.tid.mbta_app.android.util - -/** Set in MBTAGoMessagingService or retrieved on app launch in MainActivity */ -var fcmToken: String? = null diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/manageFavorites.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/manageFavorites.kt index 19a6169b71..54aa3fa8fd 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/manageFavorites.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/util/manageFavorites.kt @@ -35,7 +35,7 @@ fun manageFavorites(favoritesUseCases: FavoritesUsecases = koinInject()): Manage newValues, context, defaultDirection, - if (notificationsEnabled) fcmToken else null, + if (notificationsEnabled) fcmInstallationId else null, currentLocale, ) } diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 127fe541e4..cbffe5bd68 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -18,7 +18,7 @@ struct ContentView: View { @EnvironmentObject var viewportProvider: ViewportProvider @ObservedObject var contentVM: ContentViewModel - @ObservedObject var fcmTokenContainer = FcmTokenContainer.shared + @ObservedObject var fcmInstallationIdContainer = FcmInstallationIdContainer.shared @ObservedObject var notificationDeepLinkOwner = AppDelegate.notificationDeepLinkOwner @State private var contentHeight: CGFloat = UIScreen.current?.bounds.height ?? 0 @@ -109,16 +109,16 @@ struct ContentView: View { } .alerts($alerts) .global($globalData, errorKey: ErrorKey(sheets: [], id: "ContentView")) - .handleFcmTokenSubscriptions( - fcmToken: fcmTokenContainer.token, + .handleFcmInstallationIdSubscriptions( + fcmInstallationId: fcmInstallationIdContainer.installationId, includeAccessibility: includeAccessibility, notificationsEnabled: notificationsFlag ) - .onChange(of: fcmTokenContainer.token) { token in - favoritesVM.clearStaleFavorites(fcmToken: token) + .onChange(of: fcmInstallationIdContainer.installationId) { installationId in + favoritesVM.clearStaleFavorites(fcmInstallationId: installationId) } .onChange(of: globalData) { _ in - favoritesVM.clearStaleFavorites(fcmToken: fcmTokenContainer.token) + favoritesVM.clearStaleFavorites(fcmInstallationId: fcmInstallationIdContainer.installationId) } .onChange(of: contentVM.defaultTab) { newTab in // if we aren't on an entrypoint, then the default tab may have loaded after @@ -663,7 +663,7 @@ struct ContentView: View { updatedFavorites: favorites, context: context, defaultDirection: selectedDirection, - fcmToken: fcmTokenContainer.token, + fcmInstallationId: fcmInstallationIdContainer.installationId, ) }, navCallbacks: navCallbacks, diff --git a/iosApp/iosApp/GoogleService-Info.plist b/iosApp/iosApp/GoogleService-Info.plist index d6886410dc..edc7c62182 100644 --- a/iosApp/iosApp/GoogleService-Info.plist +++ b/iosApp/iosApp/GoogleService-Info.plist @@ -2,6 +2,8 @@ + FirebaseMessagingInstallationIdEnabled + GCM_SENDER_ID 304904690247 PLIST_VERSION diff --git a/iosApp/iosApp/IOSApp.swift b/iosApp/iosApp/IOSApp.swift index 120de6be02..e8f2b6b49d 100644 --- a/iosApp/iosApp/IOSApp.swift +++ b/iosApp/iosApp/IOSApp.swift @@ -36,13 +36,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken - Messaging.messaging().token { token, error in - if let error { - print("Error fetching FCM registration token: \(error)") - } else if let token { - FcmTokenContainer.shared.token = token - } - } + Messaging.messaging().register(completion: { _ in }) } func application( @@ -61,8 +55,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele completionHandler(.noData) } - func messaging(_: Messaging, didReceiveRegistrationToken token: String?) { - FcmTokenContainer.shared.token = token + func messaging(_: Messaging, didReceiveRegistration installationId: String?) { + FcmInstallationIdContainer.shared.installationId = installationId } func userNotificationCenter( diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index d3c3a2d91c..98a9cddb74 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -2,6 +2,8 @@ + FirebaseMessagingInstallationIdEnabled + FirebaseAutomaticScreenReportingEnabled FirebaseAppDelegateProxyEnabled diff --git a/iosApp/iosApp/Pages/Favorites/EditFavoritesPage.swift b/iosApp/iosApp/Pages/Favorites/EditFavoritesPage.swift index d120ce1bc3..6025a068eb 100644 --- a/iosApp/iosApp/Pages/Favorites/EditFavoritesPage.swift +++ b/iosApp/iosApp/Pages/Favorites/EditFavoritesPage.swift @@ -23,7 +23,7 @@ struct EditFavoritesPage: View { let toastVM: IToastViewModel let globalRepository: IGlobalRepository = RepositoryDI().global - @ObservedObject var fcmTokenContainer = FcmTokenContainer.shared + @ObservedObject var fcmInstallationIdContainer = FcmInstallationIdContainer.shared @EnvironmentObject var settingsCache: SettingsCache var groupByStop: Bool { settingsCache.get(.favoritesByStop) } @@ -36,7 +36,7 @@ struct EditFavoritesPage: View { updatedFavorites: [rsd: nil], context: .favorites, defaultDirection: rsd.direction, - fcmToken: fcmTokenContainer.token, + fcmInstallationId: fcmInstallationIdContainer.installationId, ) let labels = rsd.getLabels(globalResponse) @@ -70,7 +70,7 @@ struct EditFavoritesPage: View { updatedFavorites: [rsd: settings], context: .favorites, defaultDirection: rsd.direction, - fcmToken: fcmTokenContainer.token, + fcmInstallationId: fcmInstallationIdContainer.installationId, ) } ), diff --git a/iosApp/iosApp/Pages/More/MorePage.swift b/iosApp/iosApp/Pages/More/MorePage.swift index db44d7f404..0dc5016402 100644 --- a/iosApp/iosApp/Pages/More/MorePage.swift +++ b/iosApp/iosApp/Pages/More/MorePage.swift @@ -20,7 +20,7 @@ struct MorePage: View { @State var showingBuildNumber = false @State private var path = NavigationPath() - @ObservedObject var fcmTokenContainer = FcmTokenContainer.shared + @ObservedObject var fcmInstallationIdContainer = FcmInstallationIdContainer.shared @EnvironmentObject var settingsCache: SettingsCache private let translation = NSLocalizedString("key/current_locale", comment: "") @@ -87,9 +87,10 @@ struct MorePage: View { onChangeSetting: { setting, newValue in switch setting { case .stationAccessibility: - if settingsCache.get(.notifications), let fcmToken = fcmTokenContainer.token { + if settingsCache.get(.notifications), + let fcmInstallationId = fcmInstallationIdContainer.installationId { viewModel.updateAccessibility( - fcmToken: fcmToken, + fcmInstallationId: fcmInstallationId, includeAccessibility: newValue, locale: NSLocalizedString("key/current_locale", comment: "") ) diff --git a/iosApp/iosApp/Pages/RouteDetails/RouteStopListView.swift b/iosApp/iosApp/Pages/RouteDetails/RouteStopListView.swift index 1e1bbece1d..69aa8caa72 100644 --- a/iosApp/iosApp/Pages/RouteDetails/RouteStopListView.swift +++ b/iosApp/iosApp/Pages/RouteDetails/RouteStopListView.swift @@ -210,7 +210,7 @@ struct RouteStopListContentView: View { @State var displayedToast: ToastViewModel.Toast? @State var firstTimeToast: ToastViewModel.Toast? - @ObservedObject var fcmTokenContainer = FcmTokenContainer.shared + @ObservedObject var fcmInstallationIdContainer = FcmInstallationIdContainer.shared @EnvironmentObject var settingsCache: SettingsCache let inspection = Inspection() @@ -484,7 +484,7 @@ struct RouteStopListContentView: View { updatedFavorites: updatedValues, context: editContext, defaultDirection: selectedDirection, - fcmToken: fcmTokenContainer.token, + fcmInstallationId: fcmInstallationIdContainer.installationId, ) } } diff --git a/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredView.swift b/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredView.swift index 6a0bc069c1..40910875ba 100644 --- a/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredView.swift +++ b/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredView.swift @@ -37,7 +37,7 @@ struct StopDetailsFilteredView: View { @State var inSaveFavoritesFlow = false @State var alertSummaries: [String: AlertSummary?] = [:] - @ObservedObject var fcmTokenContainer = FcmTokenContainer.shared + @ObservedObject var fcmInstallationIdContainer = FcmInstallationIdContainer.shared @EnvironmentObject var settingsCache: SettingsCache @@ -294,7 +294,7 @@ struct StopDetailsFilteredView: View { updatedFavorites: updatedValues, context: .stopDetails, defaultDirection: routeStopDirection.direction, - fcmToken: fcmTokenContainer.token, + fcmInstallationId: fcmInstallationIdContainer.installationId, ) }, onClose: { inSaveFavoritesFlow = false }, diff --git a/iosApp/iosApp/Utils/Extensions/FavoritesUsecasesExtension.swift b/iosApp/iosApp/Utils/Extensions/FavoritesUsecasesExtension.swift index 60e2a2bae4..2c8dfa4c26 100644 --- a/iosApp/iosApp/Utils/Extensions/FavoritesUsecasesExtension.swift +++ b/iosApp/iosApp/Utils/Extensions/FavoritesUsecasesExtension.swift @@ -14,13 +14,13 @@ extension FavoritesUsecases { newValues: [RouteStopDirection: FavoriteSettings?], context: EditFavoritesContext, defaultDirection: Int32, - fcmToken: String?, + fcmInstallationId: String?, ) async throws { try await __updateRouteStopDirections( newValues: newValues as [RouteStopDirection: Any], context: context, defaultDirection: .init(int: defaultDirection), - fcmToken: fcmToken, + fcmInstallationId: fcmInstallationId, locale: NSLocalizedString("key/current_locale", comment: ""), ) } diff --git a/iosApp/iosApp/Utils/FcmInstallationIdContainer.swift b/iosApp/iosApp/Utils/FcmInstallationIdContainer.swift new file mode 100644 index 0000000000..cb0b3aaf26 --- /dev/null +++ b/iosApp/iosApp/Utils/FcmInstallationIdContainer.swift @@ -0,0 +1,14 @@ +// +// FcmInstallationIdContainer.swift +// iosApp +// +// Created by esimon on 11/28/25. +// Copyright © 2025 MBTA. All rights reserved. +// + +import SwiftUI + +class FcmInstallationIdContainer: ObservableObject { + static let shared = FcmInstallationIdContainer() + @Published var installationId: String? +} diff --git a/iosApp/iosApp/Utils/FcmTokenContainer.swift b/iosApp/iosApp/Utils/FcmTokenContainer.swift deleted file mode 100644 index 274844e534..0000000000 --- a/iosApp/iosApp/Utils/FcmTokenContainer.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FcmTokenContainer.swift -// iosApp -// -// Created by esimon on 11/28/25. -// Copyright © 2025 MBTA. All rights reserved. -// - -import SwiftUI - -class FcmTokenContainer: ObservableObject { - static let shared = FcmTokenContainer() - @Published var token: String? -} diff --git a/iosApp/iosApp/Utils/Modifiers/FcmSubscriptionModifier.swift b/iosApp/iosApp/Utils/Modifiers/FcmSubscriptionModifier.swift index 34d26df111..a586268c8d 100644 --- a/iosApp/iosApp/Utils/Modifiers/FcmSubscriptionModifier.swift +++ b/iosApp/iosApp/Utils/Modifiers/FcmSubscriptionModifier.swift @@ -11,7 +11,7 @@ import Shared import SwiftUI struct FcmSubscriptionModifier: ViewModifier { - let fcmToken: String? + let fcmInstallationId: String? let includeAccessibility: Bool let notificationsEnabled: Bool @@ -19,15 +19,15 @@ struct FcmSubscriptionModifier: ViewModifier { @State var favorites: Favorites = LoadedFavorites.last - func updateSubscriptions(_ fcmToken: String?, _ notificationsEnabled: Bool) { - if let fcmToken { + func updateSubscriptions(_ fcmInstallationId: String?, _ notificationsEnabled: Bool) { + if let fcmInstallationId { Task { let subscriptions = SubscriptionRequest.companion.fromFavorites( favorites: favorites.routeStopDirection, includeAccessibility: includeAccessibility ) try await subscriptionsRepository.updateSubscriptions( - fcmToken: fcmToken, + fcmInstallationId: fcmInstallationId, subscriptions: subscriptions, locale: NSLocalizedString("key/current_locale", comment: ""), notificationsEnabled: notificationsEnabled, @@ -39,22 +39,28 @@ struct FcmSubscriptionModifier: ViewModifier { func body(content: Content) -> some View { content .favorites($favorites) - .onAppear { updateSubscriptions(fcmToken, notificationsEnabled) } - .onChange(of: fcmToken) { newToken in updateSubscriptions(newToken, notificationsEnabled) } - .onChange(of: notificationsEnabled) { newNotifications in updateSubscriptions(fcmToken, newNotifications) } + .onAppear { updateSubscriptions(fcmInstallationId, notificationsEnabled) } + .onChange(of: fcmInstallationId) { newInstallationId in updateSubscriptions( + newInstallationId, + notificationsEnabled + ) } + .onChange(of: notificationsEnabled) { newNotifications in updateSubscriptions( + fcmInstallationId, + newNotifications + ) } .enableInjection() } } public extension View { - /** Update subscriptions on the backend when the FCM token is set or changed. */ - func handleFcmTokenSubscriptions( - fcmToken: String?, + /** Update subscriptions on the backend when the FCM installation ID is set or changed. */ + func handleFcmInstallationIdSubscriptions( + fcmInstallationId: String?, includeAccessibility: Bool, notificationsEnabled: Bool, ) -> some View { modifier(FcmSubscriptionModifier( - fcmToken: fcmToken, + fcmInstallationId: fcmInstallationId, includeAccessibility: includeAccessibility, notificationsEnabled: notificationsEnabled, )) diff --git a/iosApp/iosApp/ViewModels/IFavoritesViewModelExtension.swift b/iosApp/iosApp/ViewModels/IFavoritesViewModelExtension.swift index 6dfa8a3bfe..d7b99a8d9c 100644 --- a/iosApp/iosApp/ViewModels/IFavoritesViewModelExtension.swift +++ b/iosApp/iosApp/ViewModels/IFavoritesViewModelExtension.swift @@ -14,13 +14,13 @@ extension IFavoritesViewModel { updatedFavorites: [RouteStopDirection: FavoriteSettings?], context: EditFavoritesContext, defaultDirection: Int32, - fcmToken: String?, + fcmInstallationId: String?, ) { __updateFavorites( updatedFavorites: updatedFavorites, context: context, defaultDirection: .init(int: defaultDirection), - fcmToken: fcmToken, + fcmInstallationId: fcmInstallationId, locale: NSLocalizedString("key/current_locale", comment: ""), ) } diff --git a/iosApp/iosAppTests/Pages/Settings/MorePageTests.swift b/iosApp/iosAppTests/Pages/Settings/MorePageTests.swift index f1f7d7fc5e..2088e578c3 100644 --- a/iosApp/iosAppTests/Pages/Settings/MorePageTests.swift +++ b/iosApp/iosAppTests/Pages/Settings/MorePageTests.swift @@ -60,13 +60,13 @@ final class MorePageTests: XCTestCase { } ) - let expectedToken = "fcm_token" - FcmTokenContainer.shared.token = expectedToken + let expectedInstallationId = "fcm_installation_id" + FcmInstallationIdContainer.shared.installationId = expectedInstallationId let mockRepos = MockRepositories() mockRepos.subscriptions = MockSubscriptionsRepository( onUpdateSubscriptions: { _, _, _ in }, - onUpdateAccessibility: { token, accessibility, locale in - XCTAssertEqual(expectedToken, token) + onUpdateAccessibility: { installationId, accessibility, locale in + XCTAssertEqual(expectedInstallationId, installationId) XCTAssertTrue(accessibility.boolValue) XCTAssertEqual("en", locale) updateExp.fulfill() diff --git a/iosApp/iosAppTests/Utils/Modifiers/FcmSubscriptionModifierTests.swift b/iosApp/iosAppTests/Utils/Modifiers/FcmSubscriptionModifierTests.swift index 963eb3c444..aef0c0c56c 100644 --- a/iosApp/iosAppTests/Utils/Modifiers/FcmSubscriptionModifierTests.swift +++ b/iosApp/iosAppTests/Utils/Modifiers/FcmSubscriptionModifierTests.swift @@ -40,12 +40,12 @@ final class FcmSubscriptionModifierTests: XCTestCase { favorites: favorites.routeStopDirection, includeAccessibility: true ) - let expectedToken = "token_string" + let expectedInstallationId = "installation_id_string" let mockRepos = MockRepositories() mockRepos.subscriptions = MockSubscriptionsRepository( - onUpdateSubscriptions: { token, subs, locale in - XCTAssertEqual(expectedToken, token) + onUpdateSubscriptions: { installationId, subs, locale in + XCTAssertEqual(expectedInstallationId, installationId) XCTAssertEqual(expectedSubscriptions, subs) XCTAssertEqual("en", locale) updateExp.fulfill() @@ -54,8 +54,8 @@ final class FcmSubscriptionModifierTests: XCTestCase { ) loadKoinMocks(repositories: mockRepos) - let sut = Text("test").handleFcmTokenSubscriptions( - fcmToken: expectedToken, + let sut = Text("test").handleFcmInstallationIdSubscriptions( + fcmInstallationId: expectedInstallationId, includeAccessibility: true, notificationsEnabled: true, ) @@ -65,7 +65,7 @@ final class FcmSubscriptionModifierTests: XCTestCase { wait(for: [updateExp], timeout: 1) } - func testDoesNothingWithNoToken() { + func testDoesNothingWithNoInstallationId() { let updateExp = expectation(description: "subscriptions updated") updateExp.isInverted = true @@ -88,15 +88,15 @@ final class FcmSubscriptionModifierTests: XCTestCase { let mockRepos = MockRepositories() mockRepos.subscriptions = MockSubscriptionsRepository( onUpdateSubscriptions: { _, _, _ in - XCTFail("Should not update when token is missing") + XCTFail("Should not update when installation ID is missing") updateExp.fulfill() }, onUpdateAccessibility: { _, _, _ in } ) loadKoinMocks(repositories: mockRepos) - let sut = Text("test").handleFcmTokenSubscriptions( - fcmToken: nil, + let sut = Text("test").handleFcmInstallationIdSubscriptions( + fcmInstallationId: nil, includeAccessibility: true, notificationsEnabled: true, ) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/UpdateSubscriptionsRequests.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/UpdateSubscriptionsRequests.kt index d324f1e963..66e735b0a7 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/UpdateSubscriptionsRequests.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/UpdateSubscriptionsRequests.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable @Serializable public data class WriteSubscriptionsRequest internal constructor( - @SerialName("fcm_token") val fcmToken: String, + @SerialName("fcm_installation_id") val fcmInstallationId: String, val subscriptions: List, val locale: String?, ) @@ -16,7 +16,7 @@ internal constructor( @Serializable public data class UpdateAccessibilityRequest internal constructor( - @SerialName("fcm_token") val fcmToken: String, + @SerialName("fcm_installation_id") val fcmInstallationId: String, @SerialName("include_accessibility") val includeAccessibility: Boolean, val locale: String?, ) @@ -43,10 +43,10 @@ internal constructor( favorites: Map, includeAccessibility: Boolean = false, ): List { - val enabled = favorites.filter { it.value?.notifications?.enabled == true } + val enabled = favorites.filter { it.value.notifications.enabled } return enabled.map { (rsd, settings) -> val windows = - settings?.notifications?.windows?.map { + settings.notifications.windows.map { WindowRequest( startTime = it.startTime, endTime = it.endTime, @@ -58,7 +58,7 @@ internal constructor( stopId = rsd.stop, directionId = rsd.direction, includeAccessibility = includeAccessibility, - windows = windows ?: emptyList(), + windows = windows, ) } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SubscriptionsRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SubscriptionsRepository.kt index 8c1864d378..a4dcbe6341 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SubscriptionsRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SubscriptionsRepository.kt @@ -16,14 +16,14 @@ import org.koin.core.component.inject public interface ISubscriptionsRepository { public suspend fun updateSubscriptions( - fcmToken: String, + fcmInstallationId: String, subscriptions: List, locale: String?, notificationsEnabled: Boolean, ) public suspend fun updateAccessibility( - fcmToken: String, + fcmInstallationId: String, includeAccessibility: Boolean, locale: String?, ) @@ -34,13 +34,13 @@ internal class SubscriptionsRepository : ISubscriptionsRepository, KoinComponent private val mobileBackendClient: MobileBackendClient by inject() override suspend fun updateSubscriptions( - fcmToken: String, + fcmInstallationId: String, subscriptions: List, locale: String?, notificationsEnabled: Boolean, ) { val subscriptions = if (notificationsEnabled) subscriptions else emptyList() - val requestBody = WriteSubscriptionsRequest(fcmToken, subscriptions, locale) + val requestBody = WriteSubscriptionsRequest(fcmInstallationId, subscriptions, locale) ApiResult.runCatching { mobileBackendClient .post { @@ -53,11 +53,12 @@ internal class SubscriptionsRepository : ISubscriptionsRepository, KoinComponent } override suspend fun updateAccessibility( - fcmToken: String, + fcmInstallationId: String, includeAccessibility: Boolean, locale: String?, ) { - val requestBody = UpdateAccessibilityRequest(fcmToken, includeAccessibility, locale) + val requestBody = + UpdateAccessibilityRequest(fcmInstallationId, includeAccessibility, locale) ApiResult.runCatching { mobileBackendClient .post { @@ -77,19 +78,19 @@ public class MockSubscriptionsRepository( public val onUpdateAccessibility: (String, Boolean, String?) -> Unit = { _, _, _ -> }, ) : ISubscriptionsRepository { override suspend fun updateSubscriptions( - fcmToken: String, + fcmInstallationId: String, subscriptions: List, locale: String?, notificationsEnabled: Boolean, ) { - onUpdateSubscriptions(fcmToken, subscriptions, locale) + onUpdateSubscriptions(fcmInstallationId, subscriptions, locale) } override suspend fun updateAccessibility( - fcmToken: String, + fcmInstallationId: String, includeAccessibility: Boolean, locale: String?, ) { - onUpdateAccessibility(fcmToken, includeAccessibility, locale) + onUpdateAccessibility(fcmInstallationId, includeAccessibility, locale) } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecases.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecases.kt index af6e753f90..9b7f574459 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecases.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecases.kt @@ -41,7 +41,7 @@ public class FavoritesUsecases( newValues: Map, context: EditFavoritesContext, defaultDirection: Int?, - fcmToken: String?, + fcmInstallationId: String?, locale: String?, ) { val storedFavorites = repository.getFavorites() @@ -65,7 +65,7 @@ public class FavoritesUsecases( } } repository.setFavorites(storedFavorites.copy(routeStopDirection = currentFavorites)) - fcmToken?.let { + fcmInstallationId?.let { val settings = settingsRepository.getSettings() val subs = SubscriptionRequest.fromFavorites( @@ -74,7 +74,7 @@ public class FavoritesUsecases( ) CoroutineScope(Dispatchers.IO).launch { subscriptionsRepository.updateSubscriptions( - fcmToken, + fcmInstallationId, subs, locale, notificationsEnabled = settings[Settings.Notifications] ?: false, diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModel.kt index 55cf7a6739..97d86922e8 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModel.kt @@ -54,7 +54,7 @@ internal enum class RemovalReason { public interface IFavoritesViewModel { public val models: StateFlow - public fun clearStaleFavorites(fcmToken: String?) + public fun clearStaleFavorites(fcmInstallationId: String?) public fun dismissNotificationsHint() @@ -77,7 +77,7 @@ public interface IFavoritesViewModel { updatedFavorites: Map, context: EditFavoritesContext, defaultDirection: Int?, - fcmToken: String?, + fcmInstallationId: String?, locale: String?, ) } @@ -98,7 +98,7 @@ public class FavoritesViewModel( } public sealed interface Event { - public data class ClearStaleFavorites(val fcmToken: String?) : Event + public data class ClearStaleFavorites(val fcmInstallationId: String?) : Event public data object DismissNotificationsHint : Event @@ -110,7 +110,7 @@ public class FavoritesViewModel( val updatedFavorites: Map, val context: EditFavoritesContext, val defaultDirection: Int?, - val fcmToken: String?, + val fcmInstallationId: String?, val locale: String?, ) : Event } @@ -145,7 +145,7 @@ public class FavoritesViewModel( mutableStateOf(null) } - var fcmTokenForClearingStaleFavorites: String? by remember { mutableStateOf(null) } + var fcmInstallationIdForClearingStaleFavorites: String? by remember { mutableStateOf(null) } var hadOldPinnedRoutes: Boolean by remember { mutableStateOf(false) } var shouldShowFirstTimeToast: Boolean by remember { mutableStateOf(false) } var shouldShowNotificationsHint: Boolean by remember { mutableStateOf(false) } @@ -218,7 +218,7 @@ public class FavoritesViewModel( EventSink(eventHandlingTimeout = 2.seconds, sentryRepository = sentryRepository) { event -> when (event) { is Event.ClearStaleFavorites -> { - fcmTokenForClearingStaleFavorites = event.fcmToken + fcmInstallationIdForClearingStaleFavorites = event.fcmInstallationId } Event.DismissNotificationsHint -> { shouldShowNotificationsHint = false @@ -245,7 +245,7 @@ public class FavoritesViewModel( event.updatedFavorites, event.context, event.defaultDirection, - event.fcmToken, + event.fcmInstallationId, event.locale, ) reloadFavorites() @@ -253,14 +253,14 @@ public class FavoritesViewModel( } } - LaunchedEffect(globalData, favorites, fcmTokenForClearingStaleFavorites) { - val fcmToken = fcmTokenForClearingStaleFavorites + LaunchedEffect(globalData, favorites, fcmInstallationIdForClearingStaleFavorites) { + val fcmInstallationId = fcmInstallationIdForClearingStaleFavorites val resolvedFavorites = favorites - if (globalData == null || resolvedFavorites == null || fcmToken == null) { + if (globalData == null || resolvedFavorites == null || fcmInstallationId == null) { return@LaunchedEffect } - fcmTokenForClearingStaleFavorites = null + fcmInstallationIdForClearingStaleFavorites = null val staleFavorites = getStaleFavorites(resolvedFavorites.keys, globalData) if (staleFavorites.isNotEmpty()) { @@ -268,7 +268,7 @@ public class FavoritesViewModel( staleFavorites.mapValues { null }, EditFavoritesContext.StaleCheck, defaultDirection = null, - fcmToken, + fcmInstallationId, locale = null, ) sentryRepository.captureMessage("Clearing stale favorites") { @@ -402,8 +402,8 @@ public class FavoritesViewModel( override val models: StateFlow get() = internalModels - override fun clearStaleFavorites(fcmToken: String?): Unit = - fireEvent(Event.ClearStaleFavorites(fcmToken)) + override fun clearStaleFavorites(fcmInstallationId: String?): Unit = + fireEvent(Event.ClearStaleFavorites(fcmInstallationId)) override fun dismissNotificationsHint(): Unit = fireEvent(Event.DismissNotificationsHint) @@ -436,11 +436,17 @@ public class FavoritesViewModel( updatedFavorites: Map, context: EditFavoritesContext, defaultDirection: Int?, - fcmToken: String?, + fcmInstallationId: String?, locale: String?, ) { fireEvent( - Event.UpdateFavorites(updatedFavorites, context, defaultDirection, fcmToken, locale) + Event.UpdateFavorites( + updatedFavorites, + context, + defaultDirection, + fcmInstallationId, + locale, + ) ) } } @@ -464,8 +470,8 @@ constructor(initialState: FavoritesViewModel.State = FavoritesViewModel.State()) override val models: MutableStateFlow = MutableStateFlow(initialState) - override fun clearStaleFavorites(fcmToken: String?) { - onClearStaleFavorites(fcmToken) + override fun clearStaleFavorites(fcmInstallationId: String?) { + onClearStaleFavorites(fcmInstallationId) } override fun dismissNotificationsHint() { @@ -496,15 +502,15 @@ constructor(initialState: FavoritesViewModel.State = FavoritesViewModel.State()) onSetNow(now) } - override fun setIsFirstExposureToNewFavorites(isFirstExposure: Boolean) { - onSetIsFirstExposureToNewFavorites(isFirstExposure) + override fun setIsFirstExposureToNewFavorites(isFirst: Boolean) { + onSetIsFirstExposureToNewFavorites(isFirst) } override fun updateFavorites( updatedFavorites: Map, context: EditFavoritesContext, defaultDirection: Int?, - fcmToken: String?, + fcmInstallationId: String?, locale: String?, ) { onUpdateFavorites(updatedFavorites) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt index 62722fe70f..46616a43a9 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/viewModel/MoreViewModel.kt @@ -157,12 +157,16 @@ public class MoreViewModel( } public fun updateAccessibility( - fcmToken: String, + fcmInstallationId: String, includeAccessibility: Boolean, locale: String, ) { CoroutineScope(coroutineDispatcher).launch { - subscriptionsRepository.updateAccessibility(fcmToken, includeAccessibility, locale) + subscriptionsRepository.updateAccessibility( + fcmInstallationId, + includeAccessibility, + locale, + ) } } } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecasesTests.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecasesTests.kt index 51bf5869d9..f121cd9932 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecasesTests.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/FavoritesUsecasesTests.kt @@ -88,13 +88,13 @@ class FavoritesUsecasesTests : KoinTest { MockFavoritesRepository( buildFavorites { routeStopDirection(Route.Id("route_1"), "stop_1", 0) } ) - var token: String? = null + var installationId: String? = null var subs: List? = null var locale: String? = null val subscriptionsRepository = MockSubscriptionsRepository( - onUpdateSubscriptions = { fcmToken, subscriptions, l -> - token = fcmToken + onUpdateSubscriptions = { fcmInstallationId, subscriptions, l -> + installationId = fcmInstallationId subs = subscriptions locale = l } @@ -173,13 +173,13 @@ class FavoritesUsecasesTests : KoinTest { ), EditFavoritesContext.Favorites, 0, - "fake_token", + "fake_installation_id", "en", ) delay(250.milliseconds) - assertEquals("fake_token", token) + assertEquals("fake_installation_id", installationId) assertEquals(expectedSubs, subs) assertEquals("en", locale) } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModelTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModelTest.kt index 6fb97f98aa..e5510c70a1 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModelTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/viewModel/FavoritesViewModelTest.kt @@ -1214,7 +1214,7 @@ internal class FavoritesViewModelTest : KoinTest { mapOf(RouteStopDirection(route1.id, stop1.id, 0) to null), EditFavoritesContext.Favorites, 0, - "fcmToken", + "fcmInstallationId", "en", ) awaitItemSatisfying {