diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj index 04b561eb5..3306f2051 100644 --- a/examples/demo/App.xcodeproj/project.pbxproj +++ b/examples/demo/App.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; }; + 06E5B0ABA65D52900ECE8DE8 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B360D269E2338137D6FD023 /* LogService.swift */; }; 08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; }; 0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */; }; 0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; }; @@ -73,6 +74,7 @@ B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */; }; C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; }; C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C7A9A815CF43E7216A29908D /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7899AD7E97A0FABAF6E533 /* LogView.swift */; }; CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */; }; D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -314,11 +316,13 @@ 1155DE423DAA8B5396948C9B /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = ""; }; 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailsSection.swift; sourceTree = ""; }; 1EE449E8308FCB038408D7CF /* vine_boom.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = vine_boom.wav; sourceTree = ""; }; + 1F7899AD7E97A0FABAF6E533 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasesSection.swift; sourceTree = ""; }; 24CBCBDE9F48219D7EC827C3 /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = ""; }; 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgets.swift; sourceTree = ""; }; 291D83300C20BA3831824AFD /* OneSignalSDK */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OneSignalSDK; path = ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj; sourceTree = ""; }; + 2B360D269E2338137D6FD023 /* LogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogService.swift; sourceTree = ""; }; 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotificationDialog.swift; sourceTree = ""; }; 35F726E64F9B6817F917227C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipDialog.swift; sourceTree = ""; }; @@ -496,6 +500,7 @@ isa = PBXGroup; children = ( 0542854462194E28D9E4638D /* LiveActivityController.swift */, + 2B360D269E2338137D6FD023 /* LogService.swift */, 38138523A8A81A60A77800CA /* NotificationSender.swift */, 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */, C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */, @@ -515,6 +520,7 @@ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */, 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */, 3DD26AED9EA84B0308C1BFE8 /* LoginUserDialog.swift */, + 1F7899AD7E97A0FABAF6E533 /* LogView.swift */, 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */, D261D46C404E325CBA87A9E0 /* OSDialog.swift */, D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */, @@ -831,6 +837,8 @@ 7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */, DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */, B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */, + 06E5B0ABA65D52900ECE8DE8 /* LogService.swift in Sources */, + C7A9A815CF43E7216A29908D /* LogView.swift in Sources */, 673E0146B4B27EB81FCF50DA /* LoginUserDialog.swift in Sources */, E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */, 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */, diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift index db1d37689..7e6d6fd2c 100644 --- a/examples/demo/App/App.swift +++ b/examples/demo/App/App.swift @@ -93,7 +93,10 @@ class NotificationLifecycleHandler: NSObject, OSNotificationLifecycleListener { static let shared = NotificationLifecycleHandler() func onWillDisplay(event: OSNotificationWillDisplayEvent) { - print("[OneSignal] Notification will display: \(event.notification.title ?? "No title")") + let notification = event.notification + LogService.show( + "Notification foregroundWillDisplay: \(notification.notificationId ?? "null") \(notification.title ?? "")" + ) // Optionally modify display behavior // event.preventDefault() // Prevent automatic display // event.notification.display() // Manually display later @@ -104,8 +107,10 @@ class NotificationClickHandler: NSObject, OSNotificationClickListener { static let shared = NotificationClickHandler() func onClick(event: OSNotificationClickEvent) { - print("[OneSignal] Notification clicked: \(event.notification.title ?? "No title")") - // Handle notification click - navigate to specific screen, etc. + let notification = event.notification + LogService.show( + "Notification click: \(notification.notificationId ?? "null") \(notification.title ?? "")" + ) } } @@ -115,19 +120,19 @@ class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener { static let shared = InAppMessageLifecycleHandler() func onWillDisplay(event: OSInAppMessageWillDisplayEvent) { - print("[OneSignal] IAM will display: \(event.message.messageId)") + LogService.show("IAM willDisplay: \(event.message.messageId)") } func onDidDisplay(event: OSInAppMessageDidDisplayEvent) { - print("[OneSignal] IAM did display: \(event.message.messageId)") + LogService.show("IAM didDisplay: \(event.message.messageId)") } func onWillDismiss(event: OSInAppMessageWillDismissEvent) { - print("[OneSignal] IAM will dismiss: \(event.message.messageId)") + LogService.show("IAM willDismiss: \(event.message.messageId)") } func onDidDismiss(event: OSInAppMessageDidDismissEvent) { - print("[OneSignal] IAM did dismiss: \(event.message.messageId)") + LogService.show("IAM didDismiss: \(event.message.messageId)") } } @@ -135,7 +140,6 @@ class InAppMessageClickHandler: NSObject, OSInAppMessageClickListener { static let shared = InAppMessageClickHandler() func onClick(event: OSInAppMessageClickEvent) { - print("[OneSignal] IAM clicked: \(event.result.actionId ?? "No action ID")") - // Handle IAM click - navigate, track event, etc. + LogService.show("IAM click: \(event.message.messageId)") } } diff --git a/examples/demo/App/Services/LogService.swift b/examples/demo/App/Services/LogService.swift new file mode 100644 index 000000000..b48b6070d --- /dev/null +++ b/examples/demo/App/Services/LogService.swift @@ -0,0 +1,93 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import Foundation + +struct LogEntry: Identifiable, Equatable { + let id: Int + let time: String + let message: String +} + +/// Demo logging that marks every console line with `[OneSignal]`. +/// +/// `show` and `error` also append to the in-app Log View, which the shared Appium suite reads. +/// All methods are safe to call from any thread. +final class LogService: ObservableObject { + static let shared = LogService() + + private static let maxEntries = 100 + + // In memory only, so every launch (including a cold start from a notification tap) starts empty. + @Published private(set) var entries: [LogEntry] = [] + private var nextId = 0 + + private let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "HH:mm:ss" + return formatter + }() + + private init() {} + + static func info(_ message: String) { + print("[OneSignal] \(message)") + } + + static func show(_ message: String) { + info(message) + shared.addToView(message) + } + + static func error(_ message: String) { + FileHandle.standardError.write(Data("[OneSignal] \(message)\n".utf8)) + shared.addToView(message) + } + + func clear() { + entries.removeAll() + } + + private func addToView(_ message: String) { + let time = timeFormatter.string(from: Date()) + // Append inline on main so a queued append can't land after a later clear(). + guard Thread.isMainThread else { + DispatchQueue.main.async { self.append(time: time, message: message) } + return + } + append(time: time, message: message) + } + + private func append(time: String, message: String) { + entries.append(LogEntry(id: nextId, time: time, message: message)) + nextId += 1 + if entries.count > Self.maxEntries { + entries.removeFirst(entries.count - Self.maxEntries) + } + } +} diff --git a/examples/demo/App/ViewModels/OneSignalViewModel.swift b/examples/demo/App/ViewModels/OneSignalViewModel.swift index 0dfc20b5c..25778850c 100644 --- a/examples/demo/App/ViewModels/OneSignalViewModel.swift +++ b/examples/demo/App/ViewModels/OneSignalViewModel.swift @@ -185,7 +185,7 @@ final class OneSignalViewModel: ObservableObject { userFetchStatus = "OK" case .failure(let error): userFetchStatus = "\(aliasLabel) fetch failed: \(error)" - print("[OneSignal] /users fetch by \(aliasLabel) failed: \(error)") + LogService.info("/users fetch by \(aliasLabel) failed: \(error)") } isLoading = false } @@ -236,7 +236,7 @@ final class OneSignalViewModel: ObservableObject { if jwtAskExternalId == trimmedId { jwtAskExternalId = nil } - print("[OneSignal] Updated JWT for: \(trimmedId)") + LogService.info("Updated JWT for: \(trimmedId)") if currentUser { // The demo now holds a fresh bearer for this user, and no user-state // event is guaranteed, so refetch instead of leaving the stale failure. @@ -248,7 +248,7 @@ final class OneSignalViewModel: ObservableObject { func handleJwtInvalidated(externalId: String) { service.clearSessionJwtToken() jwtAskExternalId = externalId - print("[OneSignal] JWT invalidated for externalId: \(externalId)") + LogService.info("JWT invalidated for externalId: \(externalId)") } func logout() { @@ -267,7 +267,7 @@ final class OneSignalViewModel: ObservableObject { func setUseIdentityVerification(_ enabled: Bool) { useIdentityVerification = enabled service.useIdentityVerification = enabled - print("[OneSignal] Fetch by external_id \(enabled ? "enabled" : "disabled")") + LogService.info("Fetch by external_id \(enabled ? "enabled" : "disabled")") Task { await fetchUserDataFromApi() } } @@ -387,7 +387,7 @@ final class OneSignalViewModel: ObservableObject { func checkLocationShared() -> Bool { let shared = service.isLocationShared - print("[OneSignal] Location shared: \(shared)") + LogService.info("Location shared: \(shared)") return shared } @@ -425,17 +425,17 @@ extension OneSignalViewModel { func sendOutcome(_ name: String) { service.sendOutcome(name) - print("[OneSignal] Outcome sent: \(name)") + LogService.info("Outcome sent: \(name)") } func sendUniqueOutcome(_ name: String) { service.sendUniqueOutcome(name) - print("[OneSignal] Unique outcome sent: \(name)") + LogService.info("Unique outcome sent: \(name)") } func sendOutcome(_ name: String, value: Double) { service.sendOutcome(name, value: NSNumber(value: value)) - print("[OneSignal] Outcome sent: \(name) = \(value)") + LogService.info("Outcome sent: \(name) = \(value)") } // MARK: - In-App @@ -488,7 +488,7 @@ extension OneSignalViewModel { func trackEvent(name: String, properties: [String: Any]?) { service.trackEvent(name: name, properties: properties) - print("[OneSignal] Event tracked: \(name)") + LogService.info("Event tracked: \(name)") } // MARK: - Notifications @@ -557,6 +557,13 @@ private final class Observers: NSObject, weak var viewModel: OneSignalViewModel? func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) { + let previous = state.previous + let current = state.current + LogService.show( + "Push subscription changed: id=\(previous.id ?? "null") → \(current.id ?? "null"), " + + "optedIn=\(previous.optedIn) → \(current.optedIn), " + + "token=\(Self.truncatedToken(previous.token)) → \(Self.truncatedToken(current.token))" + ) Task { @MainActor in viewModel?.pushSubscriptionId = state.current.id viewModel?.isPushEnabled = state.current.optedIn @@ -564,12 +571,17 @@ private final class Observers: NSObject, } func onUserStateDidChange(state: OSUserChangedState) { + LogService.show( + "User changed: onesignalId=\(state.current.onesignalId ?? "null"), " + + "externalId=\(state.current.externalId ?? "null")" + ) Task { @MainActor in await viewModel?.fetchUserDataFromApi() } } func onNotificationPermissionDidChange(_ permission: Bool) { + LogService.show("Permission changed: \(permission)") Task { @MainActor in viewModel?.hasNotificationPermission = permission viewModel?.isPushEnabled = OneSignal.User.pushSubscription.optedIn @@ -581,4 +593,9 @@ private final class Observers: NSObject, viewModel?.handleJwtInvalidated(externalId: event.externalId) } } + + private static func truncatedToken(_ token: String?) -> String { + guard let token, !token.isEmpty else { return "null" } + return "\(token.prefix(8))…" + } } diff --git a/examples/demo/App/Views/Components/LogView.swift b/examples/demo/App/Views/Components/LogView.swift new file mode 100644 index 000000000..96ddb32b3 --- /dev/null +++ b/examples/demo/App/Views/Components/LogView.swift @@ -0,0 +1,143 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import SwiftUI + +/// Logcat-style panel pinned below the navigation bar. +struct LogView: View { + @ObservedObject private var logService = LogService.shared + @State private var collapsed = false + + private static let headerHeight: CGFloat = 24 + private static let bodyHeight: CGFloat = 100 + private static let bottomAnchor = "log_bottom" + + var body: some View { + VStack(spacing: 0) { + header + entryList + } + .frame(maxWidth: .infinity) + .background(OS.Color.logBackground) + .accessibilityElement(children: .contain) + .accessibilityIdentifier("log_view") + } + + private var header: some View { + HStack(spacing: 0) { + Text("LOGS (\(logService.entries.count))") + .font(.system(size: 11, weight: .bold, design: .monospaced)) + .tracking(0.5) + .foregroundColor(OS.Color.grey500) + .frame(maxWidth: .infinity, alignment: .leading) + HeaderButton( + systemImage: "trash.fill", + iconSize: 14, + label: "Clear logs", + accessibilityID: "log_clear_button", + action: logService.clear + ) + HeaderButton( + systemImage: collapsed ? "chevron.down" : "chevron.up", + iconSize: 18, + label: collapsed ? "Expand logs" : "Collapse logs", + accessibilityID: "log_toggle_button", + action: { collapsed.toggle() } + ) + } + .padding(.leading, OS.Spacing.pagePadding) + .padding(.trailing, 9) + .frame(height: Self.headerHeight) + } + + // Collapsing shrinks the body to zero height instead of removing it so the + // entries stay in the accessibility hierarchy. + private var entryList: some View { + ScrollViewReader { proxy in + ScrollView { + // Not lazy: XCUITest only sees rows that SwiftUI has instantiated. + VStack(alignment: .leading, spacing: 0) { + if logService.entries.isEmpty { + Text("No logs yet") + .foregroundColor(OS.Color.grey600) + .modifier(EntryText()) + } + ForEach(Array(logService.entries.enumerated()), id: \.element.id) { index, entry in + (Text(entry.time).foregroundColor(OS.Color.grey600) + + Text(" \(entry.message)").foregroundColor(OS.Color.logText)) + .modifier(EntryText()) + .accessibilityIdentifier("log_entry_\(index)") + } + Color.clear.frame(height: 0).id(Self.bottomAnchor) + } + .padding(.horizontal, OS.Spacing.pagePadding) + .padding(.bottom, 6) + } + .frame(height: collapsed ? 0 : Self.bodyHeight) + .clipped() + .onAppear { proxy.scrollTo(Self.bottomAnchor, anchor: .bottom) } + .onChange(of: logService.entries.last?.id) { _ in + proxy.scrollTo(Self.bottomAnchor, anchor: .bottom) + } + .onChange(of: collapsed) { isCollapsed in + if !isCollapsed { proxy.scrollTo(Self.bottomAnchor, anchor: .bottom) } + } + } + } +} + +private struct EntryText: ViewModifier { + func body(content: Content) -> some View { + content + .font(.system(size: 8, design: .monospaced)) + .lineSpacing(8 * 0.4) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } +} + +private struct HeaderButton: View { + let systemImage: String + let iconSize: CGFloat + let label: String + let accessibilityID: String + let action: () -> Void + + var body: some View { + // The header is only 24pt tall, so widen the touch target instead. + Button(action: action) { + Image(systemName: systemImage) + .font(.system(size: iconSize)) + .foregroundColor(OS.Color.grey500) + .frame(width: 32, height: 24) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(label) + .accessibilityIdentifier(accessibilityID) + } +} diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index fbaf70fd8..4c632c930 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -35,47 +35,50 @@ struct ContentView: View { var body: some View { NavigationStack { - ScrollView { - VStack(alignment: .leading, spacing: OS.Spacing.sectionGap) { - AppSection() - UserSection() - PushSection() - SendPushSection() - InAppSection() - SendIamSection() - AliasesSection() - EmailsSection() - SmsSection() - TagsSection() - OutcomesSection() - TriggersSection() - CustomEventsSection() - LocationSection() - #if !targetEnvironment(macCatalyst) - LiveActivitySection() - #endif - ActionButton( - "NEXT SCREEN", - accessibilityID: "next_screen_button" - ) { - showSecondary = true + VStack(spacing: 0) { + LogView() + ScrollView { + VStack(alignment: .leading, spacing: OS.Spacing.sectionGap) { + AppSection() + UserSection() + PushSection() + SendPushSection() + InAppSection() + SendIamSection() + AliasesSection() + EmailsSection() + SmsSection() + TagsSection() + OutcomesSection() + TriggersSection() + CustomEventsSection() + LocationSection() + #if !targetEnvironment(macCatalyst) + LiveActivitySection() + #endif + ActionButton( + "NEXT SCREEN", + accessibilityID: "next_screen_button" + ) { + showSecondary = true + } } + .padding(.horizontal, OS.Spacing.pagePadding) + .padding(.top, OS.Spacing.pagePadding) + .padding(.bottom, OS.Spacing.sectionGap) } - .padding(.horizontal, OS.Spacing.pagePadding) - .padding(.top, OS.Spacing.pagePadding) - .padding(.bottom, OS.Spacing.sectionGap) + // `main_scroll_view` is anchored to the SwiftUI `ScrollView` (not + // the inner `VStack`) so XCUITest exposes it as + // `XCUIElementTypeScrollView` with the visible viewport's rect. + // Anchoring on the inner `VStack` reported the full content rect + // (multiple screens tall), causing WDIO `swipe` to compute + // gesture coordinates outside the viewport — iOS clipped those + // to the visible region and the swipe registered as a tap on + // whatever button sat there (e.g. `send_sound_button`). The + // ScrollView identifier is read by `waitForAppReady` and by + // Android's `scrollIntoView` `scrollableElement` param. + .accessibilityIdentifier("main_scroll_view") } - // `main_scroll_view` is anchored to the SwiftUI `ScrollView` (not - // the inner `VStack`) so XCUITest exposes it as - // `XCUIElementTypeScrollView` with the visible viewport's rect. - // Anchoring on the inner `VStack` reported the full content rect - // (multiple screens tall), causing WDIO `swipe` to compute - // gesture coordinates outside the viewport — iOS clipped those - // to the visible region and the swipe registered as a tap on - // whatever button sat there (e.g. `send_sound_button`). The - // ScrollView identifier is read by `waitForAppReady` and by - // Android's `scrollIntoView` `scrollableElement` param. - .accessibilityIdentifier("main_scroll_view") .background(OS.Color.lightBackground.ignoresSafeArea()) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(OS.Color.primary, for: .navigationBar) diff --git a/examples/demo/App/Views/Theme.swift b/examples/demo/App/Views/Theme.swift index 645f5bf0b..f9858f94c 100644 --- a/examples/demo/App/Views/Theme.swift +++ b/examples/demo/App/Views/Theme.swift @@ -47,6 +47,8 @@ enum OS { static let warningBackground = SwiftUI.Color(red: 0xFF/255, green: 0xF8/255, blue: 0xE1/255) static let backdrop = SwiftUI.Color.black.opacity(0.54) static let bodyText = SwiftUI.Color(red: 0x21/255, green: 0x21/255, blue: 0x21/255) + static let logBackground = SwiftUI.Color(red: 0x1E/255, green: 0x1E/255, blue: 0x1E/255) + static let logText = SwiftUI.Color(red: 0xD4/255, green: 0xD4/255, blue: 0xD4/255) } // MARK: Spacing