diff --git a/ios/Approach/Package.swift b/ios/Approach/Package.swift index 57f813150..e1a4f49ad 100644 --- a/ios/Approach/Package.swift +++ b/ios/Approach/Package.swift @@ -253,12 +253,14 @@ let package = Package( .target( name: "AppFeature", dependencies: [ + .product(name: "AnalyticsPackageService", package: "swift-utilities"), .product(name: "AppInfoPackageService", package: "swift-utilities"), .product(name: "BundlePackageService", package: "swift-utilities"), .product(name: "FileManagerPackageService", package: "swift-utilities"), .product(name: "PasteboardPackageService", package: "swift-utilities"), .product(name: "SentryErrorReportingPackageService", package: "swift-utilities"), .product(name: "StoreReviewPackageService", package: "swift-utilities"), + .product(name: "TelemetryDeckAnalyticsPackageService", package: "swift-utilities"), .product(name: "UserDefaultsPackageService", package: "swift-utilities"), "AccessoriesOverviewFeature", "BowlersListFeature", @@ -1129,19 +1131,17 @@ let package = Package( .target( name: "AnalyticsService", dependencies: [ - .product(name: "BundlePackageServiceInterface", package: "swift-utilities"), .product(name: "Sentry", package: "sentry-cocoa"), - .product(name: "TelemetryClient", package: "SwiftClient"), "AnalyticsServiceInterface", - "ConstantsLibrary", - "PreferenceServiceInterface", ] ), .target( name: "AnalyticsServiceInterface", dependencies: [ + .product(name: "AnalyticsPackageServiceInterface", package: "swift-utilities"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "ErrorReportingClientPackageLibrary", package: "swift-utilities"), ] ), diff --git a/ios/Approach/Package.swift.toml b/ios/Approach/Package.swift.toml index edac16005..51202370f 100644 --- a/ios/Approach/Package.swift.toml +++ b/ios/Approach/Package.swift.toml @@ -12,7 +12,7 @@ supported = [ "\"17.0\"" ] [features.App] features = [ "AccessoriesOverview", "BowlersList", "Onboarding", "Settings", "StatisticsOverview" ] services = [ "Launch" ] -dependencies = [ "AppInfoPackageService", "BundlePackageService", "SentryErrorReportingPackageService", "FileManagerPackageService", "PasteboardPackageService", "StoreReviewPackageService", "UserDefaultsPackageService" ] +dependencies = [ "AnalyticsPackageService", "AppInfoPackageService", "BundlePackageService", "FileManagerPackageService", "PasteboardPackageService", "SentryErrorReportingPackageService", "StoreReviewPackageService", "TelemetryDeckAnalyticsPackageService", "UserDefaultsPackageService" ] [features.AccessoriesOverview] features = [ "AlleysList", "GearList" ] @@ -226,11 +226,9 @@ libraries = [ "Models" ] dependencies = [ "EquatablePackageLibrary", "XCTestDynamicOverlay" ] [services.Analytics] -services = [ "Preference" ] -libraries = [ "Constants" ] -dependencies = [ "BundlePackageServiceInterface", "Sentry", "TelemetryClient" ] +dependencies = [ "Sentry" ] [services.Analytics.interface] -dependencies = [ "ComposableArchitecture", "ErrorReportingClientPackageLibrary" ] +dependencies = [ "AnalyticsPackageServiceInterface", "ComposableArchitecture", "DependenciesMacros", "ErrorReportingClientPackageLibrary" ] [services.Announcements] dependencies = [ "UserDefaultsPackageServiceInterface" ] @@ -511,6 +509,12 @@ from = "1.1.0" url = "https://github.com/weichsel/ZIPFoundation.git" from = "0.9.18" +[dependencies.AnalyticsPackageService] +sharedRef = "SwiftUtilities" + +[dependencies.AnalyticsPackageServiceInterface] +sharedRef = "SwiftUtilities" + [dependencies.AppInfoPackageService] sharedRef = "SwiftUtilities" @@ -556,6 +560,9 @@ sharedRef = "SwiftUtilities" [dependencies.SwiftUIExtensionsPackageLibrary] sharedRef = "SwiftUtilities" +[dependencies.TelemetryDeckAnalyticsPackageService] +sharedRef = "SwiftUtilities" + [dependencies.TestUtilitiesPackageLibrary] sharedRef = "SwiftUtilities" diff --git a/ios/Approach/Sources/AccessoriesOverviewFeature/AccessoriesOverview.swift b/ios/Approach/Sources/AccessoriesOverviewFeature/AccessoriesOverview.swift index 410d72f18..4518c8804 100644 --- a/ios/Approach/Sources/AccessoriesOverviewFeature/AccessoriesOverview.swift +++ b/ios/Approach/Sources/AccessoriesOverviewFeature/AccessoriesOverview.swift @@ -101,7 +101,7 @@ public struct AccessoriesOverview: Reducer { public init() {} @Dependency(AlleysRepository.self) var alleys - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics @Dependency(GearRepository.self) var gear @Dependency(\.uuid) var uuid diff --git a/ios/Approach/Sources/AnalyticsService/AnalyticsService+Live.swift b/ios/Approach/Sources/AnalyticsService/AnalyticsService+Live.swift deleted file mode 100644 index 7cb7631fd..000000000 --- a/ios/Approach/Sources/AnalyticsService/AnalyticsService+Live.swift +++ /dev/null @@ -1,102 +0,0 @@ -import AnalyticsServiceInterface -import BundlePackageServiceInterface -import ConstantsLibrary -import Dependencies -import Foundation -import PreferenceServiceInterface -import Sentry -import TelemetryClient - -extension AnalyticsService: DependencyKey { - public static var liveValue: Self { - let properties = PropertyManager() - - let gameSessionID: LockIsolated = .init(nil) - - @Sendable func getOptInStatus() -> Analytics.OptInStatus { - @Dependency(\.preferences) var preferences - return Analytics.OptInStatus(rawValue: preferences.string(forKey: .analyticsOptInStatus) ?? "") ?? .optedIn - } - - @Sendable func initialize() { - @Dependency(\.bundle) var bundle - let apiKey = bundle.object(forInfoDictionaryKey: "TELEMETRY_DECK_API_KEY") as? String ?? "" - let configuration = TelemetryManagerConfiguration(appID: apiKey) - if apiKey.isEmpty { - print("Analytics disabled") - configuration.analyticsDisabled = true - } else if getOptInStatus() == .optedOut { - print("Analytics opted out") - configuration.analyticsDisabled = true - } - - TelemetryManager.initialize(with: configuration) - } - - return Self( - initialize: initialize, - setGlobalProperty: { value, key in - if let value { - await properties.setProperty(value: value, forKey: key) - } else { - await properties.removeProperty(forKey: key) - } - }, - trackEvent: { event in - let payload = (await properties.globalProperties).merging(event.payload ?? [:]) { first, _ in first } - - if let sessionEvent = event as? GameSessionTrackableEvent, - let gameSessionID = gameSessionID.value, - !(await properties.shouldRecordEvent(sessionEvent.eventId, toSession: gameSessionID)) { - return - } - - TelemetryManager.send(event.name, with: payload) - }, - breadcrumb: { breadcrumb in - let crumb = Sentry.Breadcrumb(level: .info, category: breadcrumb.category.rawValue) - crumb.message = breadcrumb.message - SentrySDK.addBreadcrumb(crumb) - }, - resetGameSessionID: { - @Dependency(\.uuid) var uuid - gameSessionID.setValue(uuid()) - }, - getOptInStatus: getOptInStatus, - setOptInStatus: { newValue in - @Dependency(\.preferences) var preferences - preferences.setString(forKey: .analyticsOptInStatus, to: newValue.rawValue) - - TelemetryManager.terminate() - initialize() - - return getOptInStatus() - }, - forceCrash: { - SentrySDK.crash() - } - ) - } -} - -actor PropertyManager { - var globalProperties: [String: String] = [:] - var sessions: [UUID: Set] = [:] - - func setProperty(value: String, forKey: String) { - globalProperties[forKey] = value - } - - func removeProperty(forKey: String) { - globalProperties[forKey] = nil - } - - func shouldRecordEvent(_ id: UUID, toSession: UUID) -> Bool { - if sessions[toSession] == nil { - sessions[toSession] = [] - } - - guard let (inserted, _) = sessions[toSession]?.insert(id) else { return false } - return inserted - } -} diff --git a/ios/Approach/Sources/AnalyticsService/BreadcrumbService+Live.swift b/ios/Approach/Sources/AnalyticsService/BreadcrumbService+Live.swift new file mode 100644 index 000000000..a062ee320 --- /dev/null +++ b/ios/Approach/Sources/AnalyticsService/BreadcrumbService+Live.swift @@ -0,0 +1,15 @@ +import AnalyticsServiceInterface +import Dependencies +import Sentry + +extension BreadcrumbService: DependencyKey { + public static var liveValue: Self { + Self( + drop: { breadcrumb in + let crumb = Sentry.Breadcrumb(level: .info, category: breadcrumb.category.rawValue) + crumb.message = breadcrumb.message + SentrySDK.addBreadcrumb(crumb) + } + ) + } +} diff --git a/ios/Approach/Sources/AnalyticsService/Crash+Live.swift b/ios/Approach/Sources/AnalyticsService/Crash+Live.swift new file mode 100644 index 000000000..7c8a8ba4a --- /dev/null +++ b/ios/Approach/Sources/AnalyticsService/Crash+Live.swift @@ -0,0 +1,9 @@ +import AnalyticsServiceInterface +import Dependencies +import Sentry + +extension CrashGenerator: DependencyKey { + public static var liveValue: Self { + return Self { SentrySDK.crash() } + } +} diff --git a/ios/Approach/Sources/AnalyticsService/GameAnalyticsService+Live.swift b/ios/Approach/Sources/AnalyticsService/GameAnalyticsService+Live.swift new file mode 100644 index 000000000..a96eef339 --- /dev/null +++ b/ios/Approach/Sources/AnalyticsService/GameAnalyticsService+Live.swift @@ -0,0 +1,38 @@ +import AnalyticsPackageServiceInterface +import AnalyticsServiceInterface +import Dependencies +import Foundation + +extension GameAnalyticsService: DependencyKey { + public static var liveValue: Self { + let sessionID = ActorIsolated(nil) + let sessions = ActorIsolated<[UUID: Set]>([:]) + + return Self( + trackEvent: { event in + if let session = await sessionID.value { + let inserted = await sessions.withValue { + $0[session, default: []].insert(event.eventId).inserted + } + + if !inserted { + return + } + } + + @Dependency(\.analytics) var analytics + let basicEvent = BasicEvent(name: event.name, payload: event.payload) + try? await analytics.trackEvent(basicEvent) + }, + resetGameSessionID: { + @Dependency(\.uuid) var uuid + await sessionID.setValue(uuid()) + } + ) + } +} + +private struct BasicEvent: TrackableEvent { + let name: String + let payload: [String: String]? +} diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/Analytics.swift b/ios/Approach/Sources/AnalyticsServiceInterface/Analytics.swift index aa4b2f3d7..cc36a3408 100644 --- a/ios/Approach/Sources/AnalyticsServiceInterface/Analytics.swift +++ b/ios/Approach/Sources/AnalyticsServiceInterface/Analytics.swift @@ -1,20 +1,9 @@ +@_exported import AnalyticsPackageServiceInterface import Foundation -public enum Analytics {} - -extension Analytics { - public enum OptInStatus: String { - case optedIn - case optedOut - } -} - -public protocol TrackableEvent { +public protocol GameSessionTrackableEvent { var name: String { get } var payload: [String: String]? { get } -} - -public protocol GameSessionTrackableEvent: TrackableEvent { var eventId: UUID { get } } diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsReducer.swift b/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsReducer.swift index ac0b8a68b..70565d7df 100644 --- a/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsReducer.swift +++ b/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsReducer.swift @@ -8,12 +8,30 @@ public struct AnalyticsReducer: Reducer { self.reducer = reducer } - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics public var body: some Reducer { Reduce { state, action in guard let event = reducer(state, action) else { return .none } - return .run { _ in await analytics.trackEvent(event) } + return .run { _ in try? await analytics.trackEvent(event) } + } + } +} + +@Reducer +public struct GameAnalyticsReducer: Reducer { + let reducer: (State, Action) -> GameSessionTrackableEvent? + + public init(reducer: @escaping (_ state: State, _ action: Action) -> GameSessionTrackableEvent?) { + self.reducer = reducer + } + + @Dependency(\.gameAnalytics) var gameAnalytics + + public var body: some Reducer { + Reduce { state, action in + guard let event = reducer(state, action) else { return .none } + return .run { _ in await gameAnalytics.trackEvent(event) } } } } diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsService+Interface.swift b/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsService+Interface.swift deleted file mode 100644 index ea031e052..000000000 --- a/ios/Approach/Sources/AnalyticsServiceInterface/AnalyticsService+Interface.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Dependencies -import Foundation - -public struct AnalyticsService: Sendable { - public var initialize: @Sendable () -> Void - public var setGlobalProperty: @Sendable (String?, String) async -> Void - public var trackEvent: @Sendable (TrackableEvent) async -> Void - public var breadcrumb: @Sendable (Breadcrumb) async -> Void - public var resetGameSessionID: @Sendable () async -> Void - public var getOptInStatus: @Sendable () -> Analytics.OptInStatus - public var setOptInStatus: @Sendable (Analytics.OptInStatus) async -> Analytics.OptInStatus - public var forceCrash: @Sendable () -> Void - - public init( - initialize: @escaping @Sendable () -> Void, - setGlobalProperty: @escaping @Sendable (String?, String) async -> Void, - trackEvent: @escaping @Sendable (TrackableEvent) async -> Void, - breadcrumb: @escaping @Sendable (Breadcrumb) async -> Void, - resetGameSessionID: @escaping @Sendable () async -> Void, - getOptInStatus: @escaping @Sendable () -> Analytics.OptInStatus, - setOptInStatus: @escaping @Sendable (Analytics.OptInStatus) async -> Analytics.OptInStatus, - forceCrash: @escaping @Sendable () -> Void - ) { - self.initialize = initialize - self.setGlobalProperty = setGlobalProperty - self.trackEvent = trackEvent - self.breadcrumb = breadcrumb - self.resetGameSessionID = resetGameSessionID - self.getOptInStatus = getOptInStatus - self.setOptInStatus = setOptInStatus - self.forceCrash = forceCrash - } - - public func setGlobalProperty(value: String?, forKey: String) async { - await self.setGlobalProperty(value, forKey) - } -} - -extension AnalyticsService: TestDependencyKey { - public static var testValue = Self( - initialize: { unimplemented("\(Self.self).initialize") }, - setGlobalProperty: { _, _ in unimplemented("\(Self.self).setGlobalProperty") }, - trackEvent: { _ in unimplemented("\(Self.self).trackEvent") }, - breadcrumb: { _ in unimplemented("\(Self.self).breadcrumb") }, - resetGameSessionID: { unimplemented("\(Self.self).resetGameSessionID") }, - getOptInStatus: { unimplemented("\(Self.self).getOptInStatus") }, - setOptInStatus: { _ in unimplemented("\(Self.self).setOptInStatus") }, - forceCrash: { unimplemented("\(Self.self).forceCrash") } - ) -} diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbReducer.swift b/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbReducer.swift index fc6644169..d14fbfe93 100644 --- a/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbReducer.swift +++ b/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbReducer.swift @@ -1,3 +1,4 @@ +import AnalyticsPackageServiceInterface import ComposableArchitecture @Reducer @@ -8,12 +9,12 @@ public struct BreadcrumbReducer: Reducer { self.reducer = reducer } - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.breadcrumbs) var breadcrumbs public var body: some Reducer { Reduce { state, action in guard let breadcrumb = reducer(state, action) else { return .none } - return .run { _ in await analytics.breadcrumb(breadcrumb) } + return .run { _ in await breadcrumbs.drop(breadcrumb) } } } } diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbService+Interface.swift b/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbService+Interface.swift new file mode 100644 index 000000000..a297e250a --- /dev/null +++ b/ios/Approach/Sources/AnalyticsServiceInterface/BreadcrumbService+Interface.swift @@ -0,0 +1,19 @@ +import Dependencies +import DependenciesMacros +import Foundation + +@DependencyClient +public struct BreadcrumbService: Sendable { + public var drop: @Sendable (Breadcrumb) async -> Void +} + +extension BreadcrumbService: TestDependencyKey { + public static var testValue = Self() +} + +extension DependencyValues { + public var breadcrumbs: BreadcrumbService { + get { self[BreadcrumbService.self] } + set { self[BreadcrumbService.self] = newValue } + } +} diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/Crash.swift b/ios/Approach/Sources/AnalyticsServiceInterface/Crash.swift new file mode 100644 index 000000000..47f10f993 --- /dev/null +++ b/ios/Approach/Sources/AnalyticsServiceInterface/Crash.swift @@ -0,0 +1,24 @@ +import Dependencies + +public struct CrashGenerator: Sendable { + private var generate: @Sendable () -> Void + + public init(_ generate: @escaping @Sendable () -> Void) { + self.generate = generate + } + + public func callAsFunction() { + self.generate() + } +} + +extension CrashGenerator: TestDependencyKey { + public static var testValue = Self { } +} + +extension DependencyValues { + public var crash: CrashGenerator { + get { self[CrashGenerator.self] } + set { self[CrashGenerator.self] = newValue } + } +} diff --git a/ios/Approach/Sources/AnalyticsServiceInterface/GameAnalyticsService+Interface.swift b/ios/Approach/Sources/AnalyticsServiceInterface/GameAnalyticsService+Interface.swift new file mode 100644 index 000000000..50fc818cf --- /dev/null +++ b/ios/Approach/Sources/AnalyticsServiceInterface/GameAnalyticsService+Interface.swift @@ -0,0 +1,20 @@ +import AnalyticsPackageServiceInterface +import Dependencies +import DependenciesMacros + +@DependencyClient +public struct GameAnalyticsService: Sendable { + public var trackEvent: @Sendable (GameSessionTrackableEvent) async -> Void + public var resetGameSessionID: @Sendable () async -> Void +} + +extension GameAnalyticsService: TestDependencyKey { + public static var testValue = Self() +} + +extension DependencyValues { + public var gameAnalytics: GameAnalyticsService { + get { self[GameAnalyticsService.self] } + set { self[GameAnalyticsService.self] = newValue } + } +} diff --git a/ios/Approach/Sources/AppFeature/App.swift b/ios/Approach/Sources/AppFeature/App.swift index f69a96610..431717928 100644 --- a/ios/Approach/Sources/AppFeature/App.swift +++ b/ios/Approach/Sources/AppFeature/App.swift @@ -6,12 +6,14 @@ import OnboardingFeature import PreferenceServiceInterface import StatisticsRepositoryInterface +import AnalyticsPackageService import AppInfoPackageService import BundlePackageService import FileManagerPackageService import PasteboardPackageService import SentryErrorReportingPackageService import StoreReviewPackageService +import TelemetryDeckAnalyticsPackageService import UserDefaultsPackageService @Reducer diff --git a/ios/Approach/Sources/BowlersListFeature/BowlersList.swift b/ios/Approach/Sources/BowlersListFeature/BowlersList.swift index 1eb18a2f5..4adcdab48 100644 --- a/ios/Approach/Sources/BowlersListFeature/BowlersList.swift +++ b/ios/Approach/Sources/BowlersListFeature/BowlersList.swift @@ -6,8 +6,8 @@ import AssetsLibrary import BowlerEditorFeature import BowlersRepositoryInterface import ComposableArchitecture -import ErrorsFeature import ErrorReportingClientPackageLibrary +import ErrorsFeature import FeatureActionLibrary import GamesListFeature import GamesRepositoryInterface @@ -127,7 +127,7 @@ public struct BowlersList: Reducer { public init() {} - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics @Dependency(AnnouncementsService.self) var announcements @Dependency(BowlersRepository.self) var bowlers @Dependency(\.calendar) var calendar diff --git a/ios/Approach/Sources/GamesEditorFeature/Game/GameDetails.swift b/ios/Approach/Sources/GamesEditorFeature/Game/GameDetails.swift index 86ec602d9..bfc8ff2c6 100644 --- a/ios/Approach/Sources/GamesEditorFeature/Game/GameDetails.swift +++ b/ios/Approach/Sources/GamesEditorFeature/Game/GameDetails.swift @@ -327,7 +327,7 @@ public struct GameDetails: Reducer { Destination() } - AnalyticsReducer { state, action in + GameAnalyticsReducer { state, action in switch action { case .internal(.destination(.presented(.scoring(.delegate(.didSetManualScore))))): guard let gameId = state.game?.id else { return nil } diff --git a/ios/Approach/Sources/GamesEditorFeature/GamesEditor.swift b/ios/Approach/Sources/GamesEditorFeature/GamesEditor.swift index eafd54c84..0805d5969 100644 --- a/ios/Approach/Sources/GamesEditorFeature/GamesEditor.swift +++ b/ios/Approach/Sources/GamesEditorFeature/GamesEditor.swift @@ -552,10 +552,6 @@ public struct GamesEditorAnalyticsReducer: Reducer { } else { return Analytics.MatchPlay.Created() } - case let .internal(.didUpdateFrame(.success(frame))): - return Analytics.Game.Updated(gameId: frame.gameId) - case let .internal(.didUpdateGame(.success(game))): - return Analytics.Game.Updated(gameId: game.id) case let .internal(.didUpdateMatchPlay(.success(matchPlay))): return Analytics.MatchPlay.Updated( withOpponent: matchPlay.opponent != nil, @@ -566,6 +562,17 @@ public struct GamesEditorAnalyticsReducer: Reducer { return nil } } + + GameAnalyticsReducer { _, action in + switch action { + case let .internal(.didUpdateGame(.success(game))): + return Analytics.Game.Updated(gameId: game.id) + case let .internal(.didUpdateFrame(.success(frame))): + return Analytics.Game.Updated(gameId: frame.gameId) + default: + return nil + } + } } } diff --git a/ios/Approach/Sources/GamesListFeature/GamesList.swift b/ios/Approach/Sources/GamesListFeature/GamesList.swift index 6d9b603f3..b048a9314 100644 --- a/ios/Approach/Sources/GamesListFeature/GamesList.swift +++ b/ios/Approach/Sources/GamesListFeature/GamesList.swift @@ -104,8 +104,8 @@ public struct GamesList: Reducer { public init() {} - @Dependency(AnalyticsService.self) var analytics @Dependency(\.dismiss) var dismiss + @Dependency(\.gameAnalytics) var gameAnalytics @Dependency(GamesRepository.self) var games @Dependency(SeriesRepository.self) var series @Dependency(TipsService.self) var tips @@ -177,7 +177,7 @@ public struct GamesList: Reducer { initialGameId: id )) - return .run { _ in await analytics.resetGameSessionID() } + return .run { _ in await gameAnalytics.resetGameSessionID() } } case let .internal(internalAction): diff --git a/ios/Approach/Sources/LaunchService/LaunchService+Live.swift b/ios/Approach/Sources/LaunchService/LaunchService+Live.swift index 8c920d890..421a08077 100644 --- a/ios/Approach/Sources/LaunchService/LaunchService+Live.swift +++ b/ios/Approach/Sources/LaunchService/LaunchService+Live.swift @@ -13,11 +13,11 @@ extension LaunchService: DependencyKey { Self( didInit: { // For sync initializars that must run before anything else in the app - @Dependency(AnalyticsService.self) var analytics - analytics.initialize() + @Dependency(\.analytics) var analytics + try? analytics.initialize() Task.detached(priority: .utility) { - await analytics.trackEvent(Analytics.App.Launched()) + try await analytics.trackEvent(Analytics.App.Launched()) } }, didLaunch: { diff --git a/ios/Approach/Sources/SeriesListFeature/Models/SeriesListError.swift b/ios/Approach/Sources/SeriesListFeature/Models/SeriesListError.swift new file mode 100644 index 000000000..35115d8af --- /dev/null +++ b/ios/Approach/Sources/SeriesListFeature/Models/SeriesListError.swift @@ -0,0 +1,13 @@ +import Foundation +import ModelsLibrary + +public enum SeriesListError: Error, LocalizedError { + case seriesNotFound(Series.ID) + + public var errorDescription: String? { + switch self { + case let .seriesNotFound(id): + return "Could not find Series with ID '\(id)'" + } + } +} diff --git a/ios/Approach/Sources/SeriesListFeature/SeriesList.swift b/ios/Approach/Sources/SeriesListFeature/SeriesList.swift index 56d3433d7..8f2a028f1 100644 --- a/ios/Approach/Sources/SeriesListFeature/SeriesList.swift +++ b/ios/Approach/Sources/SeriesListFeature/SeriesList.swift @@ -396,14 +396,3 @@ public struct SeriesList: Reducer { } } } - -public enum SeriesListError: Error, LocalizedError { - case seriesNotFound(Series.ID) - - public var errorDescription: String? { - switch self { - case let .seriesNotFound(id): - return "Could not find Series with ID '\(id)'" - } - } -} diff --git a/ios/Approach/Sources/SettingsFeature/Analytics/AnalyticsSettings.swift b/ios/Approach/Sources/SettingsFeature/Analytics/AnalyticsSettings.swift index 336c178ef..ed3221e8c 100644 --- a/ios/Approach/Sources/SettingsFeature/Analytics/AnalyticsSettings.swift +++ b/ios/Approach/Sources/SettingsFeature/Analytics/AnalyticsSettings.swift @@ -13,7 +13,7 @@ public struct AnalyticsSettings: Reducer { public var analyticsOptIn: Bool public init() { - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics switch analytics.getOptInStatus() { case .optedIn: self.analyticsOptIn = true @@ -40,7 +40,7 @@ public struct AnalyticsSettings: Reducer { public init() {} - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics public var body: some Reducer { BindingReducer() @@ -68,7 +68,7 @@ public struct AnalyticsSettings: Reducer { case .binding(\.analyticsOptIn): return .run { [optedIn = state.analyticsOptIn] send in let status = optedIn ? Analytics.OptInStatus.optedIn : Analytics.OptInStatus.optedOut - await send(.internal(.updatedOptInStatus(analytics.setOptInStatus(status)))) + await send(.internal(.updatedOptInStatus((try? analytics.setOptInStatus(status)) ?? status))) } case .delegate, .binding: @@ -123,29 +123,3 @@ public struct AnalyticsSettingsView: View { .onAppear { send(.onAppear) } } } - -#if DEBUG -struct AnalyticsSettingsPreview: PreviewProvider { - static var previews: some View { - NavigationStack { - AnalyticsSettingsView(store: .init( - initialState: withDependencies { - $0[AnalyticsService.self] = .init( - initialize: { }, - setGlobalProperty: { _, _ in }, - trackEvent: { _ in }, - breadcrumb: { _ in }, - resetGameSessionID: { }, - getOptInStatus: { .optedIn }, - setOptInStatus: { _ in .optedIn }, - forceCrash: {} - ) - } operation: { - AnalyticsSettings.State() - }, - reducer: AnalyticsSettings.init - )) - } - } -} -#endif diff --git a/ios/Approach/Sources/SettingsFeature/Settings.swift b/ios/Approach/Sources/SettingsFeature/Settings.swift index 07e99e0d7..90e33417c 100644 --- a/ios/Approach/Sources/SettingsFeature/Settings.swift +++ b/ios/Approach/Sources/SettingsFeature/Settings.swift @@ -108,8 +108,9 @@ public struct Settings: Reducer { case didTapDismissButton } - @Dependency(AnalyticsService.self) var analytics + @Dependency(\.analytics) var analytics @Dependency(AppIconService.self) var appIcon + @Dependency(\.crash) var crash @Dependency(DatabaseMockingService.self) var databaseMocking @Dependency(EmailService.self) var email @Dependency(ExportService.self) var export @@ -195,7 +196,7 @@ public struct Settings: Reducer { return .run { _ in await openURL(AppConstants.openSourceRepositoryUrl) } case .didTapForceCrashButton: - return .run { _ in analytics.forceCrash() } + return .run { _ in crash() } case .didTapAnalyticsButton: state.destination = .analytics(.init()) diff --git a/ios/Approach/Sources/ZIPServiceInterface/ZIPService+Interface.swift b/ios/Approach/Sources/ZIPServiceInterface/ZIPService+Interface.swift index af8095508..2de73891a 100644 --- a/ios/Approach/Sources/ZIPServiceInterface/ZIPService+Interface.swift +++ b/ios/Approach/Sources/ZIPServiceInterface/ZIPService+Interface.swift @@ -3,7 +3,7 @@ import DependenciesMacros import Foundation @DependencyClient -public struct ZIPService { +public struct ZIPService: Sendable { public var zipContents: @Sendable (_ ofUrls: [URL], _ to: String) throws -> URL } diff --git a/ios/Approach/Tests/AnalyticsServiceTests/AnalyticsServiceTests.swift b/ios/Approach/Tests/AnalyticsServiceTests/AnalyticsServiceTests.swift deleted file mode 100644 index b18fb0b9f..000000000 --- a/ios/Approach/Tests/AnalyticsServiceTests/AnalyticsServiceTests.swift +++ /dev/null @@ -1,13 +0,0 @@ -@testable import AnalyticsService -@testable import AnalyticsServiceInterface -import TelemetryClient -import XCTest - -final class AnalyticsServiceTests: XCTestCase { - func testInitializes() { - let analytics: AnalyticsService = .liveValue - analytics.initialize() - - XCTAssertTrue(TelemetryManager.isInitialized) - } -} diff --git a/ios/Approach/Tests/AnalyticsServiceTests/File.swift b/ios/Approach/Tests/AnalyticsServiceTests/File.swift new file mode 100644 index 000000000..136738a81 --- /dev/null +++ b/ios/Approach/Tests/AnalyticsServiceTests/File.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Joseph Roque on 2024-05-19. +// + +import Foundation