Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/demo/App/Services/OneSignalService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
17 changes: 16 additions & 1 deletion examples/demo/App/ViewModels/OneSignalViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -255,7 +262,7 @@ final class OneSignalViewModel: ObservableObject {
service.logout()
externalUserId = nil
jwtAskExternalId = nil
setLanguage("")
resetLanguagePicker()
clearUserData()
}

Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
23 changes: 21 additions & 2 deletions examples/demo/App/Views/Sections/PushSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion examples/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion examples/demo/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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

Expand Down
Loading