diff --git a/examples/demo/App/Services/OneSignalService.swift b/examples/demo/App/Services/OneSignalService.swift index d29757e0e..11d70d913 100644 --- a/examples/demo/App/Services/OneSignalService.swift +++ b/examples/demo/App/Services/OneSignalService.swift @@ -155,6 +155,7 @@ final class OneSignalService { // MARK: - Push Subscription var pushSubscriptionId: String? { OneSignal.User.pushSubscription.id } + var pushSubscriptionToken: String? { OneSignal.User.pushSubscription.token } var isPushEnabled: Bool { OneSignal.User.pushSubscription.optedIn } var hasNotificationPermission: Bool { OneSignal.Notifications.permission } diff --git a/examples/demo/App/ViewModels/OneSignalViewModel.swift b/examples/demo/App/ViewModels/OneSignalViewModel.swift index 25778850c..4cd1cc611 100644 --- a/examples/demo/App/ViewModels/OneSignalViewModel.swift +++ b/examples/demo/App/ViewModels/OneSignalViewModel.swift @@ -53,6 +53,7 @@ final class OneSignalViewModel: ObservableObject { // MARK: - Push @Published var pushSubscriptionId: String? + @Published var pushSubscriptionToken: String? @Published var isPushEnabled: Bool = false @Published var hasNotificationPermission: Bool = false @@ -86,6 +87,10 @@ final class OneSignalViewModel: ObservableObject { var loginButtonTitle: String { isLoggedIn ? "SWITCH USER" : "LOGIN USER" } + var isPushSubscribed: Bool { + isPushEnabled && hasNotificationPermission && !(pushSubscriptionToken ?? "").isEmpty + } + // MARK: - Private private let service: OneSignalService @@ -128,6 +133,7 @@ final class OneSignalViewModel: ObservableObject { func refreshState() { pushSubscriptionId = service.pushSubscriptionId + pushSubscriptionToken = service.pushSubscriptionToken isPushEnabled = service.isPushEnabled isInAppMessagesPaused = service.isInAppMessagesPaused isLocationShared = service.isLocationShared @@ -215,6 +221,7 @@ final class OneSignalViewModel: ObservableObject { isLoading = true service.login(externalId: trimmed, jwtToken: jwtToken) externalUserId = trimmed + resetLanguagePicker() if jwtToken != nil, jwtAskExternalId == trimmed { jwtAskExternalId = nil } @@ -255,7 +262,7 @@ final class OneSignalViewModel: ObservableObject { service.logout() externalUserId = nil jwtAskExternalId = nil - setLanguage("") + resetLanguagePicker() clearUserData() } @@ -264,6 +271,13 @@ final class OneSignalViewModel: ObservableObject { self.language = language } + /// Clears only the cached choice. `setLanguage("")` would send the device + /// language and overwrite the user's backend value. + private func resetLanguagePicker() { + prefs.setLanguage("") + language = "" + } + func setUseIdentityVerification(_ enabled: Bool) { useIdentityVerification = enabled service.useIdentityVerification = enabled @@ -566,6 +580,7 @@ private final class Observers: NSObject, ) Task { @MainActor in viewModel?.pushSubscriptionId = state.current.id + viewModel?.pushSubscriptionToken = state.current.token viewModel?.isPushEnabled = state.current.optedIn } } diff --git a/examples/demo/App/Views/Sections/PushSection.swift b/examples/demo/App/Views/Sections/PushSection.swift index d78dfba54..757aeb871 100644 --- a/examples/demo/App/Views/Sections/PushSection.swift +++ b/examples/demo/App/Views/Sections/PushSection.swift @@ -27,7 +27,7 @@ import SwiftUI -/// Push subscription ID, opt-in toggle, and prompt-for-permission CTA +/// Push subscription ID, subscription status, opt-in toggle, and prompt-for-permission CTA struct PushSection: View { @EnvironmentObject var viewModel: OneSignalViewModel @@ -39,6 +39,11 @@ struct PushSection: View { ) { VStack(spacing: 0) { pushIdRow + Rectangle() + .fill(OS.Color.divider) + .frame(height: OS.Layout.dividerHeight) + .padding(.vertical, 4) + pushStatusRow Rectangle() .fill(OS.Color.divider) .frame(height: OS.Layout.dividerHeight) @@ -74,9 +79,23 @@ struct PushSection: View { .padding(.vertical, 4) } + private var pushStatusRow: some View { + HStack(alignment: .center, spacing: 12) { + Text("Status") + .font(OS.Font.bodyMedium) + .foregroundColor(OS.Color.bodyText) + Spacer(minLength: 0) + Text(viewModel.isPushSubscribed ? "Subscribed" : "Unsubscribed") + .font(OS.Font.bodyMedium) + .foregroundColor(OS.Color.bodyText) + .accessibilityIdentifier("push_subscription_status") + } + .padding(.vertical, 4) + } + private var pushEnabledRow: some View { HStack(alignment: .center, spacing: 12) { - Text("Push Enabled") + Text("Opted In") .font(OS.Font.bodyMedium) .foregroundColor(OS.Color.bodyText) Spacer(minLength: 0) diff --git a/examples/demo/README.md b/examples/demo/README.md index 292202b5a..f12022972 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -8,7 +8,7 @@ The demo covers all major OneSignal SDK capabilities: - **App / Consent**: App ID display, `consent_required` and `privacy_consent` toggles - **User**: Login / logout with external user ID -- **Push Subscription**: Push subscription ID, opt-in toggle, prompt for permission +- **Push Subscription**: Push subscription ID, subscription status, opt-in toggle, prompt for permission - **Send Push Notification**: Simple / image / sound / custom notifications via the OneSignal REST API - **In-App Messaging**: Pause / resume IAM display, send IAM trigger to surface dashboard messages - **Aliases / Emails / SMS / Tags**: Add (single or multiple), remove (single or selected) diff --git a/examples/demo/build.md b/examples/demo/build.md index cfb7e6302..d3277eee3 100644 --- a/examples/demo/build.md +++ b/examples/demo/build.md @@ -58,7 +58,7 @@ The only manual step is running `xcodegen generate` after editing `project.yml`. Use a single `OneSignalViewModel` (`App/ViewModels/OneSignalViewModel.swift`) as the central state manager. There is no repository wrapper -- the view-model calls the OneSignal SDK directly through a thin `OneSignalService` singleton (`App/Services/OneSignalService.swift`). -- `@MainActor final class OneSignalViewModel: ObservableObject` with `@Published` properties for reactive state (app id, push subscription id, aliases, tags, emails, SMS, triggers, consent, location, IAM paused, `isLoading`) +- `@MainActor final class OneSignalViewModel: ObservableObject` with `@Published` properties for reactive state (app id, push subscription id and token, aliases, tags, emails, SMS, triggers, consent, location, IAM paused, `isLoading`) - The only `@Published` UI overlay state is `activeTooltip: TooltipData?`. Action dialogs are NOT in the view-model -- each section owns its own `@State` boolean and binds `.osCenteredDialog(isPresented:)` locally - `init` calls `refreshState()` and the private `setupObservers()`, which registers `OSPushSubscriptionObserver`, `OSUserStateObserver`, and `OSNotificationPermissionObserver`. SwiftUI keeps the view-model alive for the app lifetime via `@StateObject` in `App.swift`, so no manual teardown is needed - `OneSignalService` (singleton, `App/Services/OneSignalService.swift`) funnels every SDK call through one entry point and mirrors any setters the SDK doesn't expose as getters (consent flags) into `UserDefaults` @@ -103,6 +103,7 @@ Push subscription id, opt-in, notification permission, and the live `OneSignal.U - `OneSignalViewModel` exposes `promptPushPermission()` (no `isReady` gate, no separate `promptPush()` method) - `ContentView` auto-prompts on first appear via an unconditional `.task { viewModel.promptPushPermission() }` modifier on the root view -- this races the OneSignal iOS-params response so the standard alert shows before the SDK can register provisional auth - `PushSection` renders a conditional `PROMPT PUSH` button that calls `viewModel.promptPushPermission()`. The button is hidden once `hasNotificationPermission == true` +- `PushSection` rows, in order: `Push ID`, a read-only `Status` (`Subscribed` when `isPushSubscribed`, i.e. opted in AND permission granted AND a push token is present; otherwise `Unsubscribed`), and the `Opted In` toggle ### Loading State