diff --git a/Package.swift b/Package.swift index 0281a8266..f6fcab710 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,8 @@ let package = Package( .library(name: "SpecialErrorPages", targets: ["SpecialErrorPages"]), .library(name: "DuckPlayer", targets: ["DuckPlayer"]), .library(name: "PhishingDetection", targets: ["PhishingDetection"]), - .library(name: "Onboarding", targets: ["Onboarding"]) + .library(name: "Onboarding", targets: ["Onboarding"]), + .library(name: "TipKitUtils", targets: ["TipKitUtils"]) ], dependencies: [ .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "15.0.0"), @@ -429,6 +430,14 @@ let package = Package( .define("DEBUG", .when(configuration: .debug)) ] ), + .target( + name: "TipKitUtils", + dependencies: [ + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), // MARK: - Test Targets .testTarget( diff --git a/Sources/Common/Combine/CurrentValuePublisher.swift b/Sources/Common/Combine/CurrentValuePublisher.swift new file mode 100644 index 000000000..a250aea64 --- /dev/null +++ b/Sources/Common/Combine/CurrentValuePublisher.swift @@ -0,0 +1,36 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import Combine +import Foundation + +public final class CurrentValuePublisher { + + private(set) public var value: Output + private let wrappedPublisher: AnyPublisher + private var cancellable: AnyCancellable? + + public init(initialValue: Output, publisher: AnyPublisher) { + value = initialValue + wrappedPublisher = publisher + + subscribeToPublisherUpdates() + } + + private func subscribeToPublisherUpdates() { + cancellable = wrappedPublisher + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { [weak self] value in + self?.value = value + } + } +} + +extension CurrentValuePublisher: Publisher { + public func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { + + wrappedPublisher.receive(subscriber: subscriber) + } + + +} diff --git a/Sources/TipKitUtils/TipKitController.swift b/Sources/TipKitUtils/TipKitController.swift new file mode 100644 index 000000000..14c20d729 --- /dev/null +++ b/Sources/TipKitUtils/TipKitController.swift @@ -0,0 +1,94 @@ +// +// TipKitController.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log +import TipKit + +public protocol TipKitControlling { + @available(iOS 17.0, *) + func configureTipKit() + + @available(iOS 17.0, *) + func resetTipKitOnNextAppLaunch() +} + +typealias TipKitAppEventHandler = TipKitController + +public final class TipKitController { + + private let logger: Logger + private let userDefaults: UserDefaults + + private var resetTipKitOnNextLaunch: Bool { + get { + userDefaults.bool(forKey: "resetTipKitOnNextLaunch") + } + + set { + userDefaults.set(newValue, forKey: "resetTipKitOnNextLaunch") + } + } + + public init(logger: Logger, + userDefaults: UserDefaults) { + + self.logger = logger + self.userDefaults = userDefaults + } + + @available(iOS 17.0, macOS 14.0, *) + public func configureTipKit(_ configuration: [Tips.ConfigurationOption] = []) { + do { + if resetTipKitOnNextLaunch { + resetTipKit() + resetTipKitOnNextLaunch = false + } + + try Tips.configure(configuration) + + logger.debug("TipKit initialized") + } catch { + logger.error("Failed to initialize TipKit: \(error)") + } + } + + @available(iOS 17.0, macOS 14.0, *) + private func resetTipKit() { + do { + try Tips.resetDatastore() + + logger.debug("TipKit reset") + } catch { + logger.debug("Failed to reset TipKit: \(error)") + } + } + + /// Resets TipKit + /// + /// One thing that's not documented as of 2024-10-09 is that resetting TipKit must happen before it's configured. + /// When trying to reset it after it's configured we get `TipKit.TipKitError(value: TipKit.TipKitError.Value.tipsDatastoreAlreadyConfigured)`. + /// In order to make things work for us we set a user defaults value that ensures TipKit will be reset on next + /// app launch instead of directly trying to reset it here. + /// + @available(iOS 17.0, *) + public func resetTipKitOnNextAppLaunch() { + resetTipKitOnNextLaunch = true + logger.debug("TipKit will reset on next app launch") + } +}