From e76107c5bbd669eb789b1e1412a52adf93ae2402 Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Mon, 19 Jan 2026 15:00:27 +0300 Subject: [PATCH 1/5] Refactor Info --- .../Model/LocalNotification.swift | 6 +- .../FeatureFlags/FeatureFlags.swift | 5 - Sources/OversizeServices/Info/AppConfig.swift | 82 ++++++++ Sources/OversizeServices/Info/Info.swift | 176 +++++++----------- .../Extensions/ProductExtension.swift | 2 +- 5 files changed, 155 insertions(+), 116 deletions(-) create mode 100644 Sources/OversizeServices/Info/AppConfig.swift diff --git a/Sources/OversizeNotificationService/Model/LocalNotification.swift b/Sources/OversizeNotificationService/Model/LocalNotification.swift index ab4b4e2..71c3cfd 100644 --- a/Sources/OversizeNotificationService/Model/LocalNotification.swift +++ b/Sources/OversizeNotificationService/Model/LocalNotification.swift @@ -15,7 +15,7 @@ public struct LocalNotification: Sendable { timeInterval: Double, repeats: Bool = false, bundleImageName _: String? = nil, - userInfo: [String: String]? = nil + userInfo: [String: String]? = nil, ) { self.id = id scheduleType = .time @@ -39,7 +39,7 @@ public struct LocalNotification: Sendable { dateComponents: DateComponents, repeats: Bool, bundleImageName _: String? = nil, - userInfo: [String: String]? = nil + userInfo: [String: String]? = nil, ) { self.id = id scheduleType = .calendar @@ -63,7 +63,7 @@ public struct LocalNotification: Sendable { date: Date, repeats: Bool, bundleImageName _: String? = nil, - userInfo: [String: String]? = nil + userInfo: [String: String]? = nil, ) { self.id = id scheduleType = .calendar diff --git a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift index c972828..cf087f2 100644 --- a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift +++ b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift @@ -57,11 +57,6 @@ public enum FeatureFlags: Sendable { let value = PlistService.shared.getBoolFromDictionary(field: "Spotlight", dictionary: dictionaryName, plist: configName) return value } - - public static var alternateAppIcons: Int? { - let value = PlistService.shared.getIntFromDictionary(field: "AlternateAppIcons", dictionary: dictionaryName, plist: configName) - return value - } } @MainActor diff --git a/Sources/OversizeServices/Info/AppConfig.swift b/Sources/OversizeServices/Info/AppConfig.swift new file mode 100644 index 0000000..d9d291f --- /dev/null +++ b/Sources/OversizeServices/Info/AppConfig.swift @@ -0,0 +1,82 @@ +// +// Copyright © 2022 Alexander Romanov +// AppConfig.swift +// + +import Foundation + +public struct PlistConfiguration: Codable, Sendable { + public var links: Links + + private enum CodingKeys: String, CodingKey, Sendable { + case links = "Links" + } + + public struct Links: Codable, Sendable { + public var app: App + public var developer: Developer + public var company: Company + + private enum CodingKeys: String, CodingKey, Sendable { + case app = "App" + case developer = "Developer" + case company = "Company" + } + + public struct App: Codable, Hashable, Sendable { + public var telegramChat: String? + public var appStoreId: String + + private enum CodingKeys: String, CodingKey, Sendable { + case telegramChat = "TelegramChat" + case appStoreId = "AppStoreID" + } + } + + public struct Developer: Codable, Hashable, Sendable { + public var name: String? + public var url: String? + public var email: String? + public var facebook: String? + public var telegram: String? + + private enum CodingKeys: String, CodingKey, Sendable { + case name = "Name" + case url = "Url" + case email = "Email" + case facebook = "Facebook" + case telegram = "Telegram" + } + } + + public struct Company: Codable, Hashable, Sendable { + public var name: String? + public var urlString: String? + public var email: String? + public var appStoreId: String + public var facebook: String? + public var telegram: String? + public var dribbble: String? + public var instagram: String? + public var twitter: String? + public var cdnString: String? + + public var url: URL? { + URL(string: urlString ?? "") + } + + private enum CodingKeys: String, CodingKey { + case name = "Name" + case urlString = "Url" + case email = "Email" + case appStoreId = "AppStoreID" + case facebook = "Facebook" + case telegram = "Telegram" + case dribbble = "Dribbble" + case instagram = "Instagram" + case twitter = "Twitter" + case cdnString = "CDNUrl" + } + } + } +} diff --git a/Sources/OversizeServices/Info/Info.swift b/Sources/OversizeServices/Info/Info.swift index b56dbb9..edfe2a3 100644 --- a/Sources/OversizeServices/Info/Info.swift +++ b/Sources/OversizeServices/Info/Info.swift @@ -4,11 +4,7 @@ // import Foundation -import OversizeModels import SwiftUI -#if canImport(UIKit) -import UIKit -#endif // swiftlint:disable all @@ -55,11 +51,15 @@ public enum Info: Sendable { } public static var appStoreID: String? { - links?.app.appStoreId + guard let id = all?.links.app.appStoreId else { + print("[Info] Warning: appStoreId is not set in AppConfig.plist") + return nil + } + return id } public static var appStoreIDInt: Int? { - guard let appStoreID = links?.app.appStoreId else { return nil } + guard let appStoreID = all?.links.app.appStoreId else { return nil } return Int(appStoreID) } @@ -68,7 +68,7 @@ public enum Info: Sendable { } public static var telegramChatID: String? { - links?.app.telegramChat + all?.links.app.telegramChat } public static var iconName: String? { @@ -79,54 +79,75 @@ public enum Info: Sendable { else { return nil } return iconFileName } + + public static var icon: Image? { + if let iconName, let uiImage = UIImage(named: iconName) { + Image(uiImage: uiImage) + } else { + nil + } + } + + public static var alternateIconsNames: [String] { + guard let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any], + let alternateIcons = icons["CFBundleAlternateIcons"] as? [String: Any] + else { + return [] + } + return Array(alternateIcons.keys).sorted() + } + + @MainActor + public static var alternateIconName: String? { + #if os(iOS) + UIApplication.shared.alternateIconName + #else + nil + #endif + } } public enum developer: Sendable { public static var url: String? { - links?.developer.url + all?.links.developer.url } public static var email: String? { - links?.developer.email + all?.links.developer.email } public static var name: String? { - links?.developer.name + all?.links.developer.name } } public enum company: Sendable { public static var url: URL? { - links?.company.url + all?.links.company.url } public static var appStoreID: String? { - links?.company.appStoreId + all?.links.company.appStoreId } public static var twitterID: String? { - links?.company.twitter + all?.links.company.twitter } public static var dribbbleID: String? { - links?.company.dribbble + all?.links.company.dribbble } public static var instagramID: String? { - links?.company.instagram - } - - @available(*, deprecated, message: "Use instagramID instead") - public static var tnstagramID: String? { - instagramID + all?.links.company.instagram } public static var facebookID: String? { - links?.company.facebook + all?.links.company.facebook } public static var telegramID: String? { - links?.company.telegram + all?.links.company.telegram } } @@ -161,14 +182,31 @@ public enum Info: Sendable { return url } + public static var appUrl: URL? { + guard let companyUrl = company.url, + let appStoreId = app.appStoreID + else { return nil } + return URL(string: "\(companyUrl.absoluteString)/\(appStoreId)") + } + public static var appPrivacyPolicyUrl: URL? { - let url = URL(string: "\(links?.app.urlString ?? "")/privacy-policy") - return url + guard let appUrl else { return nil } + return URL(string: "\(appUrl.absoluteString)/privacy-policy") } public static var appTermsOfUseUrl: URL? { - let url = URL(string: "\(links?.app.urlString ?? "")/terms-and-conditions") - return url + guard let appUrl else { return nil } + return URL(string: "\(appUrl.absoluteString)/terms-and-conditions") + } + + public static var companyCdnUrl: URL? { + guard let cdnString = all?.links.company.cdnString else { return nil } + return URL(string: cdnString) + } + + public static var companyEmail: URL? { + guard let email = all?.links.company.email else { return nil } + return URL(string: "mailto:\(email)") } public static var companyTelegram: URL? { @@ -202,86 +240,10 @@ public enum Info: Sendable { } } - public static var apps: [PlistConfiguration.App] { - guard let filePath = Bundle.main.url(forResource: configName, withExtension: "plist") else { - fatalError("Couldn't find file \(configName).plist'.") - } - let data = try! Data(contentsOf: filePath) - let decoder: PropertyListDecoder = .init() - do { - let decodeData = try decoder.decode(PlistConfiguration.self, from: data) - return decodeData.apps - } catch { - return [] - } - } - - public static var links: PlistConfiguration.Links? { - guard let filePath = Bundle.main.url(forResource: configName, withExtension: "plist") else { - fatalError("Couldn't find file \(configName).plist'.") - } - let data = try! Data(contentsOf: filePath) - let decoder: PropertyListDecoder = .init() - do { - let decodeData = try decoder.decode(PlistConfiguration.self, from: data) - return decodeData.links - } catch { - return nil - } - } - - public enum store: Sendable { - public static var features: [PlistConfiguration.Store.StoreFeature] { - guard let filePath = Bundle.main.url(forResource: configName, withExtension: "plist") else { - fatalError("Couldn't find file \(configName).plist'.") - } - let data = try! Data(contentsOf: filePath) - let decoder: PropertyListDecoder = .init() - do { - let decodeData = try decoder.decode(PlistConfiguration.self, from: data) - return decodeData.store.features - } catch { - return [] - } - } - - public static func parseConfig() -> PlistConfiguration { - let url = Bundle.main.url(forResource: configName, withExtension: "plist")! - let data = try! Data(contentsOf: url) - let decoder: PropertyListDecoder = .init() - return try! decoder.decode(PlistConfiguration.self, from: data) - } - - public static var bannerLabel: String { - let value = PlistService.shared.getStringFromDictionary(field: "BannerLabel", dictionary: storeDictionaryName, plist: configName) - return value ?? "" - } - - public static var subscriptionsName: String { - let value = PlistService.shared.getStringFromDictionary(field: "SubscriptionsName", dictionary: storeDictionaryName, plist: configName) - return value ?? "" - } - - public static var subscriptionsDescription: String { - let value = PlistService.shared.getStringFromDictionary(field: "SubscriptionsDescription", dictionary: storeDictionaryName, plist: configName) - return value ?? "" - } - - public static var secretKey: String { - let value = PlistService.shared.getStringFromDictionary(field: "SecretKey", dictionary: storeDictionaryName, plist: configName) - return value ?? "" - } - - public static var productIdentifiers: [String] { - let value = PlistService.shared.getStringArrayFromDictionary(field: "ProductIdentifiers", dictionary: storeDictionaryName, plist: configName) - return value - } - } - - public static var all: PlistConfiguration? { - let url = Bundle.main.url(forResource: configName, withExtension: "plist")! - let data = try! Data(contentsOf: url) - let decoder: PropertyListDecoder = .init() - return try? decoder.decode(PlistConfiguration.self, from: data) + private static var all: PlistConfiguration? { + guard let url = Bundle.main.url(forResource: configName, withExtension: "plist"), + let data = try? Data(contentsOf: url) + else { return nil } + return try? PropertyListDecoder().decode(PlistConfiguration.self, from: data) } } diff --git a/Sources/OversizeStoreService/Extensions/ProductExtension.swift b/Sources/OversizeStoreService/Extensions/ProductExtension.swift index d5b7c12..4f9efba 100644 --- a/Sources/OversizeStoreService/Extensions/ProductExtension.swift +++ b/Sources/OversizeStoreService/Extensions/ProductExtension.swift @@ -122,7 +122,7 @@ public extension Product { var price = displayPrice if let unit = subscription?.subscriptionPeriod.unit { if #available(iOS 15.4, macOS 12.3, tvOS 15.4, *) { - price = self.displayPrice + " / " + unit.localizedDescription.lowercased() + price = displayPrice + " / " + unit.localizedDescription.lowercased() } else { switch unit { case .day: From ec0cece8fbbd1e71cadff4b1ebb6b73d63db18b6 Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Mon, 19 Jan 2026 15:23:06 +0300 Subject: [PATCH 2/5] Add support all platforms --- Sources/OversizeServices/Info/Info.swift | 400 ++++++++++++++++------- 1 file changed, 290 insertions(+), 110 deletions(-) diff --git a/Sources/OversizeServices/Info/Info.swift b/Sources/OversizeServices/Info/Info.swift index edfe2a3..88601f6 100644 --- a/Sources/OversizeServices/Info/Info.swift +++ b/Sources/OversizeServices/Info/Info.swift @@ -4,15 +4,19 @@ // import Foundation +import OversizeCore import SwiftUI -// swiftlint:disable all +#if os(watchOS) +import WatchKit +#endif public enum Info: Sendable { private static let configName = "AppConfig" - private static let storeDictionaryName = "Store" - public enum app: Sendable { + // MARK: - App + + public enum App: Sendable { public static var version: String? { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String } @@ -23,26 +27,32 @@ public enum Info: Sendable { @MainActor public static var device: String? { - #if os(iOS) - return UIDevice.current.model + #if os(iOS) || os(tvOS) || os(visionOS) + UIDevice.current.model + #elseif os(watchOS) + WKInterfaceDevice.current().model + #elseif os(macOS) + Host.current().localizedName #else - return nil + nil #endif } @MainActor - public static var system: String? { - #if os(iOS) - return UIDevice.current.systemName + " " + UIDevice.current.systemVersion + public static var osVersion: String? { + #if os(iOS) || os(tvOS) || os(visionOS) + UIDevice.current.systemName + " " + UIDevice.current.systemVersion + #elseif os(watchOS) + WKInterfaceDevice.current().systemName + " " + WKInterfaceDevice.current().systemVersion #elseif os(macOS) - let osVersion = ProcessInfo.processInfo.operatingSystemVersion - return "macOS \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" + let version = ProcessInfo.processInfo.operatingSystemVersion + return "macOS \(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" #else - return nil + nil #endif } - public static var language: String? { + public static var localeIdentifier: String? { Locale.current.identifier } @@ -50,25 +60,19 @@ public enum Info: Sendable { Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String } - public static var appStoreID: String? { - guard let id = all?.links.app.appStoreId else { - print("[Info] Warning: appStoreId is not set in AppConfig.plist") + public static var appStoreId: String? { + guard let id = configuration?.links.app.appStoreId, !id.isEmpty else { return nil } return id } - public static var appStoreIDInt: Int? { - guard let appStoreID = all?.links.app.appStoreId else { return nil } - return Int(appStoreID) - } - - public static var bundleID: String? { + public static var bundleId: String? { Bundle.main.bundleIdentifier } - public static var telegramChatID: String? { - all?.links.app.telegramChat + public static var telegramChatId: String? { + configuration?.links.app.telegramChat } public static var iconName: String? { @@ -81,14 +85,14 @@ public enum Info: Sendable { } public static var icon: Image? { - if let iconName, let uiImage = UIImage(named: iconName) { - Image(uiImage: uiImage) + if let iconName { + Image(iconName) } else { nil } } - public static var alternateIconsNames: [String] { + public static var alternateIconNames: [String] { guard let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any], let alternateIcons = icons["CFBundleAlternateIcons"] as? [String: Any] else { @@ -105,145 +109,321 @@ public enum Info: Sendable { nil #endif } - } - public enum developer: Sendable { - public static var url: String? { - all?.links.developer.url + public static var appStoreUrl: URL? { + guard let appStoreId else { return nil } + return URL(string: "https://itunes.apple.com/us/app/apple-store/id\(appStoreId)") } - public static var email: String? { - all?.links.developer.email + public static var appStoreReviewUrl: URL? { + guard let appStoreId else { return nil } + return URL(string: "itms-apps:itunes.apple.com/us/app/apple-store/id\(appStoreId)&action=write-review") } - public static var name: String? { - all?.links.developer.name + public static var websiteUrl: URL? { + guard let companyUrl = Company.url, let appStoreId else { return nil } + return URL(string: "\(companyUrl.absoluteString)/\(appStoreId)") } - } - public enum company: Sendable { - public static var url: URL? { - all?.links.company.url + public static var privacyPolicyUrl: URL? { + guard let websiteUrl else { return nil } + return URL(string: "\(websiteUrl.absoluteString)/privacy-policy") } - public static var appStoreID: String? { - all?.links.company.appStoreId + public static var termsOfUseUrl: URL? { + guard let websiteUrl else { return nil } + return URL(string: "\(websiteUrl.absoluteString)/terms-and-conditions") } - public static var twitterID: String? { - all?.links.company.twitter + public static var telegramChatUrl: URL? { + guard let telegramChatId else { return nil } + return URL(string: "https://t.me/\(telegramChatId)") } + } - public static var dribbbleID: String? { - all?.links.company.dribbble + // MARK: - Developer + + public enum Developer: Sendable { + public static var website: String? { + configuration?.links.developer.url } - public static var instagramID: String? { - all?.links.company.instagram + public static var email: String? { + configuration?.links.developer.email } - public static var facebookID: String? { - all?.links.company.facebook + public static var name: String? { + configuration?.links.developer.name } - public static var telegramID: String? { - all?.links.company.telegram + public static var emailUrl: URL? { + guard let email else { return nil } + return URL(string: "mailto:\(email)") } - } - public enum url: Sendable { - public static var appStoreReview: URL? { - guard app.appStoreID != nil else { return nil } - let url = URL(string: "itms-apps:itunes.apple.com/us/app/apple-store/id\(app.appStoreID!)?mt=8&action=write-review") - return url + public static var appsUrl: URL? { + guard let appStoreId = Company.appStoreId else { return nil } + return URL(string: "itms-apps://itunes.apple.com/developer/id\(appStoreId)") } + } + + // MARK: - Company - public static var appInstallShare: URL? { - guard app.appStoreID != nil else { return nil } - let url = URL(string: "https://itunes.apple.com/us/app/apple-store/id\(app.appStoreID!)") - return url + public enum Company: Sendable { + public static var url: URL? { + configuration?.links.company.url } - public static var developerSendMail: URL? { - guard developer.email != nil else { return nil } - let mail = URL(string: "mailto:\(developer.email!)") - return mail + public static var appStoreId: String? { + configuration?.links.company.appStoreId } - public static var developerAllApps: URL? { - guard company.appStoreID != nil else { return nil } - let url = URL(string: "itms-apps://itunes.apple.com/developer/id\(company.appStoreID!)") - return url + public static var twitterUsername: String? { + configuration?.links.company.twitter } - public static var appTelegramChat: URL? { - guard app.telegramChatID != nil else { return nil } - let url = URL(string: "https://t.me/\(app.telegramChatID!)") - return url + public static var dribbbleUsername: String? { + configuration?.links.company.dribbble } - public static var appUrl: URL? { - guard let companyUrl = company.url, - let appStoreId = app.appStoreID - else { return nil } - return URL(string: "\(companyUrl.absoluteString)/\(appStoreId)") + public static var instagramUsername: String? { + configuration?.links.company.instagram } - public static var appPrivacyPolicyUrl: URL? { - guard let appUrl else { return nil } - return URL(string: "\(appUrl.absoluteString)/privacy-policy") + public static var facebookUsername: String? { + configuration?.links.company.facebook } - public static var appTermsOfUseUrl: URL? { - guard let appUrl else { return nil } - return URL(string: "\(appUrl.absoluteString)/terms-and-conditions") + public static var telegramUsername: String? { + configuration?.links.company.telegram } - public static var companyCdnUrl: URL? { - guard let cdnString = all?.links.company.cdnString else { return nil } + public static var cdnUrl: URL? { + guard let cdnString = configuration?.links.company.cdnString else { return nil } return URL(string: cdnString) } - public static var companyEmail: URL? { - guard let email = all?.links.company.email else { return nil } + public static var emailUrl: URL? { + guard let email = configuration?.links.company.email else { return nil } return URL(string: "mailto:\(email)") } - public static var companyTelegram: URL? { - guard let id = company.telegramID else { return nil } - guard let url = URL(string: "https://www.t.me/\(id)") else { return nil } - return url + public static var telegramUrl: URL? { + guard let telegramUsername else { return nil } + return URL(string: "https://t.me/\(telegramUsername)") } - public static var companyFacebook: URL? { - guard let id = company.facebookID else { return nil } - guard let url = URL(string: "https://www.facebook.com/\(id)") else { return nil } - return url + public static var facebookUrl: URL? { + guard let facebookUsername else { return nil } + return URL(string: "https://facebook.com/\(facebookUsername)") } - public static var companyTwitter: URL? { - guard let id = company.twitterID else { return nil } - guard let url = URL(string: "https://www.twitter.com/\(id)") else { return nil } - return url + public static var twitterUrl: URL? { + guard let twitterUsername else { return nil } + return URL(string: "https://twitter.com/\(twitterUsername)") } - public static var companyDribbble: URL? { - guard let id = company.dribbbleID else { return nil } - guard let url = URL(string: "https://www.dribbble.com/\(id)") else { return nil } - return url + public static var dribbbleUrl: URL? { + guard let dribbbleUsername else { return nil } + return URL(string: "https://dribbble.com/\(dribbbleUsername)") } - public static var companyInstagram: URL? { - guard let id = company.instagramID else { return nil } - guard let url = URL(string: "https://www.instagram.com/\(id)") else { return nil } - return url + public static var instagramUrl: URL? { + guard let instagramUsername else { return nil } + return URL(string: "https://instagram.com/\(instagramUsername)") } } - private static var all: PlistConfiguration? { + // MARK: - Private + + private static var configuration: PlistConfiguration? { guard let url = Bundle.main.url(forResource: configName, withExtension: "plist"), let data = try? Data(contentsOf: url) - else { return nil } + else { + return nil + } return try? PropertyListDecoder().decode(PlistConfiguration.self, from: data) } } + +// MARK: - Deprecated Compatibility + + public extension Info { + @available(*, deprecated, renamed: "App") + typealias app = App + + @available(*, deprecated, renamed: "Developer") + typealias developer = Developer + + @available(*, deprecated, renamed: "Company") + typealias company = Company + + @available(*, deprecated, message: "URLs moved to App, Developer, Company") + typealias url = URLs + } + + public extension Info.App { + @available(*, deprecated, renamed: "appStoreId") + static var appStoreID: String? { appStoreId } + + @available(*, deprecated, renamed: "bundleId") + static var bundleID: String? { bundleId } + + @available(*, deprecated, renamed: "telegramChatId") + static var telegramChatID: String? { telegramChatId } + + @available(*, deprecated, renamed: "alternateIconNames") + static var alternateIconsNames: [String] { alternateIconNames } + + @available(*, deprecated, renamed: "osVersion") + @MainActor static var system: String? { osVersion } + + @available(*, deprecated, renamed: "localeIdentifier") + static var language: String? { localeIdentifier } + + @available(*, deprecated, renamed: "appStoreUrl") + static var appStoreURL: URL? { appStoreUrl } + + @available(*, deprecated, renamed: "appStoreReviewUrl") + static var appStoreReviewURL: URL? { appStoreReviewUrl } + + @available(*, deprecated, renamed: "websiteUrl") + static var websiteURL: URL? { websiteUrl } + + @available(*, deprecated, renamed: "privacyPolicyUrl") + static var privacyPolicyURL: URL? { privacyPolicyUrl } + + @available(*, deprecated, renamed: "termsOfUseUrl") + static var termsOfUseURL: URL? { termsOfUseUrl } + + @available(*, deprecated, renamed: "telegramChatUrl") + static var telegramChatURL: URL? { telegramChatUrl } + } + + public extension Info.Developer { + @available(*, deprecated, renamed: "website") + static var url: String? { website } + + @available(*, deprecated, renamed: "emailUrl") + static var emailURL: URL? { emailUrl } + + @available(*, deprecated, renamed: "appsUrl") + static var appsURL: URL? { appsUrl } + } + + public extension Info.Company { + @available(*, deprecated, renamed: "appStoreId") + static var appStoreID: String? { appStoreId } + + @available(*, deprecated, renamed: "twitterUsername") + static var twitterID: String? { twitterUsername } + + @available(*, deprecated, renamed: "dribbbleUsername") + static var dribbbleID: String? { dribbbleUsername } + + @available(*, deprecated, renamed: "instagramUsername") + static var instagramID: String? { instagramUsername } + + @available(*, deprecated, renamed: "facebookUsername") + static var facebookID: String? { facebookUsername } + + @available(*, deprecated, renamed: "telegramUsername") + static var telegramID: String? { telegramUsername } + + @available(*, deprecated, renamed: "cdnUrl") + static var cdnURL: URL? { cdnUrl } + + @available(*, deprecated, renamed: "emailUrl") + static var emailURL: URL? { emailUrl } + + @available(*, deprecated, renamed: "telegramUrl") + static var telegramURL: URL? { telegramUrl } + + @available(*, deprecated, renamed: "facebookUrl") + static var facebookURL: URL? { facebookUrl } + + @available(*, deprecated, renamed: "twitterUrl") + static var twitterURL: URL? { twitterUrl } + + @available(*, deprecated, renamed: "dribbbleUrl") + static var dribbbleURL: URL? { dribbbleUrl } + + @available(*, deprecated, renamed: "instagramUrl") + static var instagramURL: URL? { instagramUrl } + } + +// MARK: - Deprecated URLs Enum + + @available(*, deprecated, message: "Use Info.App, Info.Developer, Info.Company instead") + public extension Info { + enum URLs: Sendable { + @available(*, deprecated, renamed: "Info.App.appStoreReviewUrl") + public static var appStoreReview: URL? { App.appStoreReviewUrl } + + @available(*, deprecated, renamed: "Info.App.appStoreUrl") + public static var appStore: URL? { App.appStoreUrl } + + @available(*, deprecated, renamed: "Info.Developer.emailUrl") + public static var developerEmail: URL? { Developer.emailUrl } + + @available(*, deprecated, renamed: "Info.Developer.appsUrl") + public static var developerApps: URL? { Developer.appsUrl } + + @available(*, deprecated, renamed: "Info.App.telegramChatUrl") + public static var telegramChat: URL? { App.telegramChatUrl } + + @available(*, deprecated, renamed: "Info.App.websiteUrl") + public static var app: URL? { App.websiteUrl } + + @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") + public static var privacyPolicy: URL? { App.privacyPolicyUrl } + + @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") + public static var termsOfUse: URL? { App.termsOfUseUrl } + + @available(*, deprecated, renamed: "Info.Company.cdnUrl") + public static var companyCDN: URL? { Company.cdnUrl } + + @available(*, deprecated, renamed: "Info.Company.emailUrl") + public static var companyEmail: URL? { Company.emailUrl } + + @available(*, deprecated, renamed: "Info.Company.telegramUrl") + public static var companyTelegram: URL? { Company.telegramUrl } + + @available(*, deprecated, renamed: "Info.Company.facebookUrl") + public static var companyFacebook: URL? { Company.facebookUrl } + + @available(*, deprecated, renamed: "Info.Company.twitterUrl") + public static var companyTwitter: URL? { Company.twitterUrl } + + @available(*, deprecated, renamed: "Info.Company.dribbbleUrl") + public static var companyDribbble: URL? { Company.dribbbleUrl } + + @available(*, deprecated, renamed: "Info.Company.instagramUrl") + public static var companyInstagram: URL? { Company.instagramUrl } + + @available(*, deprecated, renamed: "Info.App.appStoreUrl") + public static var appInstallShare: URL? { App.appStoreUrl } + + @available(*, deprecated, renamed: "Info.Developer.emailUrl") + public static var developerSendMail: URL? { Developer.emailUrl } + + @available(*, deprecated, renamed: "Info.Developer.appsUrl") + public static var developerAllApps: URL? { Developer.appsUrl } + + @available(*, deprecated, renamed: "Info.App.telegramChatUrl") + public static var appTelegramChat: URL? { App.telegramChatUrl } + + @available(*, deprecated, renamed: "Info.App.websiteUrl") + public static var appUrl: URL? { App.websiteUrl } + + @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") + public static var appPrivacyPolicyUrl: URL? { App.privacyPolicyUrl } + + @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") + public static var appTermsOfUseUrl: URL? { App.termsOfUseUrl } + + @available(*, deprecated, renamed: "Info.Company.cdnUrl") + public static var companyCdnUrl: URL? { Company.cdnUrl } + } + } From 997772b2792162528cac4d67458321d2d5d0a3c1 Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Thu, 22 Jan 2026 17:33:49 +0300 Subject: [PATCH 3/5] Remove keys --- .../FeatureFlags/FeatureFlags.swift | 80 ++--- Sources/OversizeServices/Info/AppConfig.swift | 82 ----- .../OversizeServices/Info/Deprecated.swift | 338 ++++++++++++++++++ Sources/OversizeServices/Info/Info.swift | 274 ++++---------- .../OversizeServices/Info/PlistService.swift | 70 ---- .../AppStateService/AppStateService.swift | 2 +- .../SettingsService/SettingsService.swift | 94 ----- 7 files changed, 436 insertions(+), 504 deletions(-) delete mode 100644 Sources/OversizeServices/Info/AppConfig.swift create mode 100644 Sources/OversizeServices/Info/Deprecated.swift delete mode 100644 Sources/OversizeServices/Info/PlistService.swift diff --git a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift index cf087f2..1352da5 100644 --- a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift +++ b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift @@ -5,95 +5,67 @@ import Foundation -// swiftlint:disable line_length type_name - public enum FeatureFlags: Sendable { - private static let configName = "AppConfig" - private static let dictionaryName = "FeatureFlags" + + private static var featureFlagsDict: [String: Any]? { + Bundle.main.infoDictionary?["FeatureFlags"] as? [String: Any] + } @MainActor - public enum app: Sendable { - public static var appearance: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Apperance", dictionary: dictionaryName, plist: configName) + private static func getBool(_ key: String) -> Bool? { + if let value = featureFlagsDict?[key] as? Bool { return value } + return PlistService.shared.getBoolFromDictionary( + field: key, + dictionary: "FeatureFlags", + plist: "AppConfig", + ) + } - @available(*, deprecated, message: "Use appearance instead") - public static var apperance: Bool? { - appearance + @MainActor + public enum app: Sendable { + public static var appearance: Bool? { + getBool("Apperance") } public static var storeKit: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "StoreKit", dictionary: dictionaryName, plist: configName) - return value + getBool("StoreKit") } public static var сloudKit: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "CloudKit", dictionary: dictionaryName, plist: configName) - return value + getBool("CloudKit") } public static var healthKit: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "HealthKit", dictionary: dictionaryName, plist: configName) - return value + getBool("HealthKit") } public static var notifications: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Notifications", dictionary: dictionaryName, plist: configName) - return value + getBool("Notifications") } public static var vibration: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Vibration", dictionary: dictionaryName, plist: configName) - return value + getBool("Vibration") } public static var sounds: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Sounds", dictionary: dictionaryName, plist: configName) - return value - } - - public static var spotlight: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Spotlight", dictionary: dictionaryName, plist: configName) - return value + getBool("Sounds") } } @MainActor public enum secure { public static var faceID: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "FaceID", dictionary: dictionaryName, plist: configName) - return value + getBool("FaceID") } public static var lookscreen: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "Lookscreen", dictionary: dictionaryName, plist: configName) - return value - } - - public static var CVVCodes: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "CVVCodes", dictionary: dictionaryName, plist: configName) - return value - } - - public static var alertSecureCodes: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "AlertPINCode", dictionary: dictionaryName, plist: configName) - return value + getBool("Lookscreen") } - + public static var blurMinimize: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "BlurMinimize", dictionary: dictionaryName, plist: configName) - return value - } - - public static var bruteForceSecure: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "BruteForceSecure", dictionary: dictionaryName, plist: configName) - return value - } - - public static var photoBreaker: Bool? { - let value = PlistService.shared.getBoolFromDictionary(field: "PhotoBreaker", dictionary: dictionaryName, plist: configName) - return value + getBool("BlurMinimize") } } } diff --git a/Sources/OversizeServices/Info/AppConfig.swift b/Sources/OversizeServices/Info/AppConfig.swift deleted file mode 100644 index d9d291f..0000000 --- a/Sources/OversizeServices/Info/AppConfig.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright © 2022 Alexander Romanov -// AppConfig.swift -// - -import Foundation - -public struct PlistConfiguration: Codable, Sendable { - public var links: Links - - private enum CodingKeys: String, CodingKey, Sendable { - case links = "Links" - } - - public struct Links: Codable, Sendable { - public var app: App - public var developer: Developer - public var company: Company - - private enum CodingKeys: String, CodingKey, Sendable { - case app = "App" - case developer = "Developer" - case company = "Company" - } - - public struct App: Codable, Hashable, Sendable { - public var telegramChat: String? - public var appStoreId: String - - private enum CodingKeys: String, CodingKey, Sendable { - case telegramChat = "TelegramChat" - case appStoreId = "AppStoreID" - } - } - - public struct Developer: Codable, Hashable, Sendable { - public var name: String? - public var url: String? - public var email: String? - public var facebook: String? - public var telegram: String? - - private enum CodingKeys: String, CodingKey, Sendable { - case name = "Name" - case url = "Url" - case email = "Email" - case facebook = "Facebook" - case telegram = "Telegram" - } - } - - public struct Company: Codable, Hashable, Sendable { - public var name: String? - public var urlString: String? - public var email: String? - public var appStoreId: String - public var facebook: String? - public var telegram: String? - public var dribbble: String? - public var instagram: String? - public var twitter: String? - public var cdnString: String? - - public var url: URL? { - URL(string: urlString ?? "") - } - - private enum CodingKeys: String, CodingKey { - case name = "Name" - case urlString = "Url" - case email = "Email" - case appStoreId = "AppStoreID" - case facebook = "Facebook" - case telegram = "Telegram" - case dribbble = "Dribbble" - case instagram = "Instagram" - case twitter = "Twitter" - case cdnString = "CDNUrl" - } - } - } -} diff --git a/Sources/OversizeServices/Info/Deprecated.swift b/Sources/OversizeServices/Info/Deprecated.swift new file mode 100644 index 0000000..4729e31 --- /dev/null +++ b/Sources/OversizeServices/Info/Deprecated.swift @@ -0,0 +1,338 @@ +// +// Copyright © 2022 Alexander Romanov +// Deprecated.swift +// + +import Foundation + +// MARK: - Deprecated Compatibility + +public extension Info { + @available(*, deprecated, renamed: "App") + typealias app = App + + @available(*, deprecated, renamed: "Developer") + typealias developer = Developer + + @available(*, deprecated, renamed: "Company") + typealias company = Company + + @available(*, deprecated, message: "URLs moved to App, Developer, Company") + typealias url = URLs +} + +public extension Info.App { + @available(*, deprecated, renamed: "appStoreId") + static var appStoreID: String? { appStoreId } + + @available(*, deprecated, renamed: "bundleId") + static var bundleID: String? { bundleId } + + @available(*, deprecated, renamed: "telegramChatId") + static var telegramChatID: String? { telegramChatId } + + @available(*, deprecated, renamed: "alternateIconNames") + static var alternateIconsNames: [String] { alternateIconNames } + + @available(*, deprecated, renamed: "osVersion") + @MainActor static var system: String? { osVersion } + + @available(*, deprecated, renamed: "localeIdentifier") + static var language: String? { localeIdentifier } + + @available(*, deprecated, renamed: "appStoreUrl") + static var appStoreURL: URL? { appStoreUrl } + + @available(*, deprecated, renamed: "appStoreReviewUrl") + static var appStoreReviewURL: URL? { appStoreReviewUrl } + + @available(*, deprecated, renamed: "websiteUrl") + static var websiteURL: URL? { websiteUrl } + + @available(*, deprecated, renamed: "privacyPolicyUrl") + static var privacyPolicyURL: URL? { privacyPolicyUrl } + + @available(*, deprecated, renamed: "termsOfUseUrl") + static var termsOfUseURL: URL? { termsOfUseUrl } + + @available(*, deprecated, renamed: "telegramChatUrl") + static var telegramChatURL: URL? { telegramChatUrl } +} + +public extension Info.Developer { + @available(*, deprecated, renamed: "websiteUrl") + static var url: String? { websiteUrl?.absoluteString } + + @available(*, deprecated, renamed: "websiteUrl") + static var website: String? { websiteUrl?.absoluteString } + + @available(*, deprecated, renamed: "emailUrl") + static var emailURL: URL? { emailUrl } + + @available(*, deprecated, renamed: "appsUrl") + static var appsURL: URL? { appsUrl } +} + +public extension Info.Company { + @available(*, deprecated, renamed: "websiteUrl") + static var url: URL? { websiteUrl } + + @available(*, deprecated, renamed: "appStoreId") + static var appStoreID: String? { appStoreId } + + @available(*, deprecated, renamed: "twitterUsername") + static var twitterID: String? { twitterUsername } + + @available(*, deprecated, renamed: "dribbbleUsername") + static var dribbbleID: String? { dribbbleUsername } + + @available(*, deprecated, renamed: "instagramUsername") + static var instagramID: String? { instagramUsername } + + @available(*, deprecated, renamed: "facebookUsername") + static var facebookID: String? { facebookUsername } + + @available(*, deprecated, renamed: "telegramUsername") + static var telegramID: String? { telegramUsername } + + @available(*, deprecated, renamed: "cdnUrl") + static var cdnURL: URL? { cdnUrl } + + @available(*, deprecated, renamed: "emailUrl") + static var emailURL: URL? { emailUrl } + + @available(*, deprecated, renamed: "telegramUrl") + static var telegramURL: URL? { telegramUrl } + + @available(*, deprecated, renamed: "facebookUrl") + static var facebookURL: URL? { facebookUrl } + + @available(*, deprecated, renamed: "twitterUrl") + static var twitterURL: URL? { twitterUrl } + + @available(*, deprecated, renamed: "dribbbleUrl") + static var dribbbleURL: URL? { dribbbleUrl } + + @available(*, deprecated, renamed: "instagramUrl") + static var instagramURL: URL? { instagramUrl } +} + +// MARK: - Deprecated URLs Enum + +@available(*, deprecated, message: "Use Info.App, Info.Developer, Info.Company instead") +public extension Info { + enum URLs: Sendable { + @available(*, deprecated, renamed: "Info.App.appStoreReviewUrl") + public static var appStoreReview: URL? { App.appStoreReviewUrl } + + @available(*, deprecated, renamed: "Info.App.appStoreUrl") + public static var appStore: URL? { App.appStoreUrl } + + @available(*, deprecated, renamed: "Info.Developer.emailUrl") + public static var developerEmail: URL? { Developer.emailUrl } + + @available(*, deprecated, renamed: "Info.Developer.appsUrl") + public static var developerApps: URL? { Developer.appsUrl } + + @available(*, deprecated, renamed: "Info.App.telegramChatUrl") + public static var telegramChat: URL? { App.telegramChatUrl } + + @available(*, deprecated, renamed: "Info.App.websiteUrl") + public static var app: URL? { App.websiteUrl } + + @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") + public static var privacyPolicy: URL? { App.privacyPolicyUrl } + + @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") + public static var termsOfUse: URL? { App.termsOfUseUrl } + + @available(*, deprecated, renamed: "Info.Company.cdnUrl") + public static var companyCDN: URL? { Company.cdnUrl } + + @available(*, deprecated, renamed: "Info.Company.emailUrl") + public static var companyEmail: URL? { Company.emailUrl } + + @available(*, deprecated, renamed: "Info.Company.telegramUrl") + public static var companyTelegram: URL? { Company.telegramUrl } + + @available(*, deprecated, renamed: "Info.Company.facebookUrl") + public static var companyFacebook: URL? { Company.facebookUrl } + + @available(*, deprecated, renamed: "Info.Company.twitterUrl") + public static var companyTwitter: URL? { Company.twitterUrl } + + @available(*, deprecated, renamed: "Info.Company.dribbbleUrl") + public static var companyDribbble: URL? { Company.dribbbleUrl } + + @available(*, deprecated, renamed: "Info.Company.instagramUrl") + public static var companyInstagram: URL? { Company.instagramUrl } + + @available(*, deprecated, renamed: "Info.App.appStoreUrl") + public static var appInstallShare: URL? { App.appStoreUrl } + + @available(*, deprecated, renamed: "Info.Developer.emailUrl") + public static var developerSendMail: URL? { Developer.emailUrl } + + @available(*, deprecated, renamed: "Info.Developer.appsUrl") + public static var developerAllApps: URL? { Developer.appsUrl } + + @available(*, deprecated, renamed: "Info.App.telegramChatUrl") + public static var appTelegramChat: URL? { App.telegramChatUrl } + + @available(*, deprecated, renamed: "Info.App.websiteUrl") + public static var appUrl: URL? { App.websiteUrl } + + @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") + public static var appPrivacyPolicyUrl: URL? { App.privacyPolicyUrl } + + @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") + public static var appTermsOfUseUrl: URL? { App.termsOfUseUrl } + + @available(*, deprecated, renamed: "Info.Company.cdnUrl") + public static var companyCdnUrl: URL? { Company.cdnUrl } + } +} + +// MARK: - Links + +public struct Links: Codable, Sendable { + public var app: App? + public var developer: Developer + public var company: Company + + private enum CodingKeys: String, CodingKey, Sendable { + case app = "App" + case developer = "Developer" + case company = "Company" + } + + public struct App: Codable, Hashable, Sendable { + public var telegramChat: String? + public var appStoreId: String + + private enum CodingKeys: String, CodingKey, Sendable { + case telegramChat = "TelegramChat" + case appStoreId = "AppStoreID" + } + } + + public struct Developer: Codable, Hashable, Sendable { + public var name: String? + public var url: String? + public var email: String? + public var facebook: String? + public var telegram: String? + + private enum CodingKeys: String, CodingKey, Sendable { + case name = "Name" + case url = "Url" + case email = "Email" + case facebook = "Facebook" + case telegram = "Telegram" + } + } + + public struct Company: Codable, Hashable, Sendable { + public var name: String? + public var urlString: String? + public var email: String? + public var appStoreId: String + public var facebook: String? + public var telegram: String? + public var dribbble: String? + public var instagram: String? + public var twitter: String? + public var cdnString: String? + + public var url: URL? { + URL(string: urlString ?? "") + } + + private enum CodingKeys: String, CodingKey { + case name = "Name" + case urlString = "Url" + case email = "Email" + case appStoreId = "AppStoreID" + case facebook = "Facebook" + case telegram = "Telegram" + case dribbble = "Dribbble" + case instagram = "Instagram" + case twitter = "Twitter" + case cdnString = "CDNUrl" + } + } +} + +// MARK: - PlistConfiguration (Legacy) + +public struct PlistConfiguration: Codable, Sendable { + public var links: Links + + private enum CodingKeys: String, CodingKey, Sendable { + case links = "Links" + } +} + +public final class PlistService: Sendable { + public static let shared: PlistService = .init() + public init() {} + + public func getStringArrayFromDictionary(field: String, dictionary: String, plist: String) -> [String] { + guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { + return [] + } + let plist = NSDictionary(contentsOfFile: filePath) + guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { + return [] + } + let value: [String] = links[field] as? [String] ?? [] + return value + } + + public func getBoolFromDictionary(field: String, dictionary: String, plist: String) -> Bool? { + guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { + return nil + } + let plist = NSDictionary(contentsOfFile: filePath) + guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { + return nil + } + let value: Bool? = links[field] as? Bool + return value + } + + public func getIntFromDictionary(field: String, dictionary: String, plist: String) -> Int? { + guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { + return nil + } + let plist = NSDictionary(contentsOfFile: filePath) + guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { + return nil + } + let value: Int? = links[field] as? Int + return value + } + + public func getStringFromDictionary(field: String, dictionary: String, plist: String) -> String? { + guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { + return nil + } + let plist = NSDictionary(contentsOfFile: filePath) + guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { + return nil + } + let value: String? = links[field] as? String + return value + } + + public func getString(field: String, plist: String) -> String? { + guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { + return nil + } + let plist = NSDictionary(contentsOfFile: filePath) + guard let value = plist?.object(forKey: field) as? String else { + return nil + } + return value + } +} diff --git a/Sources/OversizeServices/Info/Info.swift b/Sources/OversizeServices/Info/Info.swift index 88601f6..4b2bfbd 100644 --- a/Sources/OversizeServices/Info/Info.swift +++ b/Sources/OversizeServices/Info/Info.swift @@ -6,7 +6,6 @@ import Foundation import OversizeCore import SwiftUI - #if os(watchOS) import WatchKit #endif @@ -61,7 +60,10 @@ public enum Info: Sendable { } public static var appStoreId: String? { - guard let id = configuration?.links.app.appStoreId, !id.isEmpty else { + if let id = Bundle.main.object(forInfoDictionaryKey: "AppStoreId") as? String, !id.isEmpty { + return id + } + guard let id = linksConfiguration?.app?.appStoreId, !id.isEmpty else { return nil } return id @@ -72,7 +74,10 @@ public enum Info: Sendable { } public static var telegramChatId: String? { - configuration?.links.app.telegramChat + if let id = Bundle.main.object(forInfoDictionaryKey: "TelegramChatId") as? String, !id.isEmpty { + return id + } + return linksConfiguration?.app?.telegramChat } public static var iconName: String? { @@ -121,7 +126,7 @@ public enum Info: Sendable { } public static var websiteUrl: URL? { - guard let companyUrl = Company.url, let appStoreId else { return nil } + guard let companyUrl = Company.websiteUrl, let appStoreId else { return nil } return URL(string: "\(companyUrl.absoluteString)/\(appStoreId)") } @@ -144,16 +149,22 @@ public enum Info: Sendable { // MARK: - Developer public enum Developer: Sendable { - public static var website: String? { - configuration?.links.developer.url + private static var developerDict: [String: Any]? { + Bundle.main.infoDictionary?["Developer"] as? [String: Any] + } + + public static var websiteUrl: URL? { + let urlString = developerDict?["WebsiteUrl"] as? String ?? linksConfiguration?.developer.url + guard let urlString else { return nil } + return URL(string: urlString.hasPrefix("http") ? urlString : "https://\(urlString)") } public static var email: String? { - configuration?.links.developer.email + developerDict?["Email"] as? String ?? linksConfiguration?.developer.email } public static var name: String? { - configuration?.links.developer.name + developerDict?["Name"] as? String ?? linksConfiguration?.developer.name } public static var emailUrl: URL? { @@ -170,41 +181,52 @@ public enum Info: Sendable { // MARK: - Company public enum Company: Sendable { - public static var url: URL? { - configuration?.links.company.url + private static var companyDict: [String: Any]? { + Bundle.main.infoDictionary?["Company"] as? [String: Any] + } + + public static var websiteUrl: URL? { + if let urlString = companyDict?["WebsiteUrl"] as? String { + return URL(string: urlString) + } + return linksConfiguration?.company.url } public static var appStoreId: String? { - configuration?.links.company.appStoreId + companyDict?["AppStoreId"] as? String ?? linksConfiguration?.company.appStoreId } public static var twitterUsername: String? { - configuration?.links.company.twitter + companyDict?["Twitter"] as? String ?? linksConfiguration?.company.twitter } public static var dribbbleUsername: String? { - configuration?.links.company.dribbble + companyDict?["Dribbble"] as? String ?? linksConfiguration?.company.dribbble } public static var instagramUsername: String? { - configuration?.links.company.instagram + companyDict?["Instagram"] as? String ?? linksConfiguration?.company.instagram } public static var facebookUsername: String? { - configuration?.links.company.facebook + companyDict?["Facebook"] as? String ?? linksConfiguration?.company.facebook } public static var telegramUsername: String? { - configuration?.links.company.telegram + companyDict?["Telegram"] as? String ?? linksConfiguration?.company.telegram } public static var cdnUrl: URL? { - guard let cdnString = configuration?.links.company.cdnString else { return nil } + if let cdnString = companyDict?["CdnUrl"] as? String { + return URL(string: cdnString) + } + guard let cdnString = linksConfiguration?.company.cdnString else { return nil } return URL(string: cdnString) } public static var emailUrl: URL? { - guard let email = configuration?.links.company.email else { return nil } + let email = companyDict?["Email"] as? String ?? linksConfiguration?.company.email + guard let email else { return nil } return URL(string: "mailto:\(email)") } @@ -236,194 +258,40 @@ public enum Info: Sendable { // MARK: - Private - private static var configuration: PlistConfiguration? { + private static var linksConfiguration: Links? { + let info = Bundle.main.infoDictionary + + if let developerDict = info?["Developer"] as? [String: Any], + let companyDict = info?["Company"] as? [String: Any] + { + let linksDict: [String: Any] = [ + "Developer": developerDict, + "Company": companyDict, + ] + if let data = try? JSONSerialization.data(withJSONObject: linksDict) { + return try? JSONDecoder().decode(Links.self, from: data) + } + } + + if let linksDict = info?["Links"] as? [String: Any], + let data = try? JSONSerialization.data(withJSONObject: linksDict) + { + return try? JSONDecoder().decode(Links.self, from: data) + } + + if let configDict = info?["AppConfig"] as? [String: Any], + let data = try? JSONSerialization.data(withJSONObject: configDict), + let config = try? JSONDecoder().decode(PlistConfiguration.self, from: data) + { + return config.links + } + guard let url = Bundle.main.url(forResource: configName, withExtension: "plist"), - let data = try? Data(contentsOf: url) + let data = try? Data(contentsOf: url), + let config = try? PropertyListDecoder().decode(PlistConfiguration.self, from: data) else { return nil } - return try? PropertyListDecoder().decode(PlistConfiguration.self, from: data) + return config.links } } - -// MARK: - Deprecated Compatibility - - public extension Info { - @available(*, deprecated, renamed: "App") - typealias app = App - - @available(*, deprecated, renamed: "Developer") - typealias developer = Developer - - @available(*, deprecated, renamed: "Company") - typealias company = Company - - @available(*, deprecated, message: "URLs moved to App, Developer, Company") - typealias url = URLs - } - - public extension Info.App { - @available(*, deprecated, renamed: "appStoreId") - static var appStoreID: String? { appStoreId } - - @available(*, deprecated, renamed: "bundleId") - static var bundleID: String? { bundleId } - - @available(*, deprecated, renamed: "telegramChatId") - static var telegramChatID: String? { telegramChatId } - - @available(*, deprecated, renamed: "alternateIconNames") - static var alternateIconsNames: [String] { alternateIconNames } - - @available(*, deprecated, renamed: "osVersion") - @MainActor static var system: String? { osVersion } - - @available(*, deprecated, renamed: "localeIdentifier") - static var language: String? { localeIdentifier } - - @available(*, deprecated, renamed: "appStoreUrl") - static var appStoreURL: URL? { appStoreUrl } - - @available(*, deprecated, renamed: "appStoreReviewUrl") - static var appStoreReviewURL: URL? { appStoreReviewUrl } - - @available(*, deprecated, renamed: "websiteUrl") - static var websiteURL: URL? { websiteUrl } - - @available(*, deprecated, renamed: "privacyPolicyUrl") - static var privacyPolicyURL: URL? { privacyPolicyUrl } - - @available(*, deprecated, renamed: "termsOfUseUrl") - static var termsOfUseURL: URL? { termsOfUseUrl } - - @available(*, deprecated, renamed: "telegramChatUrl") - static var telegramChatURL: URL? { telegramChatUrl } - } - - public extension Info.Developer { - @available(*, deprecated, renamed: "website") - static var url: String? { website } - - @available(*, deprecated, renamed: "emailUrl") - static var emailURL: URL? { emailUrl } - - @available(*, deprecated, renamed: "appsUrl") - static var appsURL: URL? { appsUrl } - } - - public extension Info.Company { - @available(*, deprecated, renamed: "appStoreId") - static var appStoreID: String? { appStoreId } - - @available(*, deprecated, renamed: "twitterUsername") - static var twitterID: String? { twitterUsername } - - @available(*, deprecated, renamed: "dribbbleUsername") - static var dribbbleID: String? { dribbbleUsername } - - @available(*, deprecated, renamed: "instagramUsername") - static var instagramID: String? { instagramUsername } - - @available(*, deprecated, renamed: "facebookUsername") - static var facebookID: String? { facebookUsername } - - @available(*, deprecated, renamed: "telegramUsername") - static var telegramID: String? { telegramUsername } - - @available(*, deprecated, renamed: "cdnUrl") - static var cdnURL: URL? { cdnUrl } - - @available(*, deprecated, renamed: "emailUrl") - static var emailURL: URL? { emailUrl } - - @available(*, deprecated, renamed: "telegramUrl") - static var telegramURL: URL? { telegramUrl } - - @available(*, deprecated, renamed: "facebookUrl") - static var facebookURL: URL? { facebookUrl } - - @available(*, deprecated, renamed: "twitterUrl") - static var twitterURL: URL? { twitterUrl } - - @available(*, deprecated, renamed: "dribbbleUrl") - static var dribbbleURL: URL? { dribbbleUrl } - - @available(*, deprecated, renamed: "instagramUrl") - static var instagramURL: URL? { instagramUrl } - } - -// MARK: - Deprecated URLs Enum - - @available(*, deprecated, message: "Use Info.App, Info.Developer, Info.Company instead") - public extension Info { - enum URLs: Sendable { - @available(*, deprecated, renamed: "Info.App.appStoreReviewUrl") - public static var appStoreReview: URL? { App.appStoreReviewUrl } - - @available(*, deprecated, renamed: "Info.App.appStoreUrl") - public static var appStore: URL? { App.appStoreUrl } - - @available(*, deprecated, renamed: "Info.Developer.emailUrl") - public static var developerEmail: URL? { Developer.emailUrl } - - @available(*, deprecated, renamed: "Info.Developer.appsUrl") - public static var developerApps: URL? { Developer.appsUrl } - - @available(*, deprecated, renamed: "Info.App.telegramChatUrl") - public static var telegramChat: URL? { App.telegramChatUrl } - - @available(*, deprecated, renamed: "Info.App.websiteUrl") - public static var app: URL? { App.websiteUrl } - - @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") - public static var privacyPolicy: URL? { App.privacyPolicyUrl } - - @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") - public static var termsOfUse: URL? { App.termsOfUseUrl } - - @available(*, deprecated, renamed: "Info.Company.cdnUrl") - public static var companyCDN: URL? { Company.cdnUrl } - - @available(*, deprecated, renamed: "Info.Company.emailUrl") - public static var companyEmail: URL? { Company.emailUrl } - - @available(*, deprecated, renamed: "Info.Company.telegramUrl") - public static var companyTelegram: URL? { Company.telegramUrl } - - @available(*, deprecated, renamed: "Info.Company.facebookUrl") - public static var companyFacebook: URL? { Company.facebookUrl } - - @available(*, deprecated, renamed: "Info.Company.twitterUrl") - public static var companyTwitter: URL? { Company.twitterUrl } - - @available(*, deprecated, renamed: "Info.Company.dribbbleUrl") - public static var companyDribbble: URL? { Company.dribbbleUrl } - - @available(*, deprecated, renamed: "Info.Company.instagramUrl") - public static var companyInstagram: URL? { Company.instagramUrl } - - @available(*, deprecated, renamed: "Info.App.appStoreUrl") - public static var appInstallShare: URL? { App.appStoreUrl } - - @available(*, deprecated, renamed: "Info.Developer.emailUrl") - public static var developerSendMail: URL? { Developer.emailUrl } - - @available(*, deprecated, renamed: "Info.Developer.appsUrl") - public static var developerAllApps: URL? { Developer.appsUrl } - - @available(*, deprecated, renamed: "Info.App.telegramChatUrl") - public static var appTelegramChat: URL? { App.telegramChatUrl } - - @available(*, deprecated, renamed: "Info.App.websiteUrl") - public static var appUrl: URL? { App.websiteUrl } - - @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") - public static var appPrivacyPolicyUrl: URL? { App.privacyPolicyUrl } - - @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") - public static var appTermsOfUseUrl: URL? { App.termsOfUseUrl } - - @available(*, deprecated, renamed: "Info.Company.cdnUrl") - public static var companyCdnUrl: URL? { Company.cdnUrl } - } - } diff --git a/Sources/OversizeServices/Info/PlistService.swift b/Sources/OversizeServices/Info/PlistService.swift deleted file mode 100644 index f7ce9dc..0000000 --- a/Sources/OversizeServices/Info/PlistService.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright © 2022 Alexander Romanov -// PlistService.swift -// - -import Foundation - -public final class PlistService: Sendable { - public static let shared: PlistService = .init() - public init() {} - - public func getStringArrayFromDictionary(field: String, dictionary: String, plist: String) -> [String] { - guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { - fatalError("Couldn't find file \(plist).plist'.") - } - let plist = NSDictionary(contentsOfFile: filePath) - guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { - fatalError("Couldn't find dictionary '\(dictionary)' in '\(String(describing: plist)).plist'.") - } - let value: [String] = links[field] as? [String] ?? [] - return value - } - - public func getBoolFromDictionary(field: String, dictionary: String, plist: String) -> Bool? { - guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { - fatalError("Couldn't find file \(plist).plist'.") - } - let plist = NSDictionary(contentsOfFile: filePath) - guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { - fatalError("Couldn't find dictionary '\(dictionary)' in '\(String(describing: plist)).plist'.") - } - let value: Bool? = links[field] as? Bool - return value - } - - public func getIntFromDictionary(field: String, dictionary: String, plist: String) -> Int? { - guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { - fatalError("Couldn't find file \(plist).plist'.") - } - let plist = NSDictionary(contentsOfFile: filePath) - guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { - fatalError("Couldn't find dictionary '\(dictionary)' in '\(String(describing: plist)).plist'.") - } - let value: Int? = links[field] as? Int - return value - } - - public func getStringFromDictionary(field: String, dictionary: String, plist: String) -> String? { - guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { - fatalError("Couldn't find file \(plist).plist'.") - } - let plist = NSDictionary(contentsOfFile: filePath) - guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { - fatalError("Couldn't find dictionary '\(dictionary)' in '\(String(describing: plist)).plist'.") - } - let value: String? = links[field] as? String - return value - } - - public func getString(field: String, plist: String) -> String? { - guard let filePath = Bundle.main.path(forResource: plist, ofType: "plist") else { - fatalError("Couldn't find file \(plist).plist'.") - } - let plist = NSDictionary(contentsOfFile: filePath) - guard let value = plist?.object(forKey: field) as? String else { - fatalError("Couldn't find key '\(field)' in '\(String(describing: plist)).plist'.") - } - return value - } -} diff --git a/Sources/OversizeServices/Services/AppStateService/AppStateService.swift b/Sources/OversizeServices/Services/AppStateService/AppStateService.swift index f367581..b1bb1ac 100644 --- a/Sources/OversizeServices/Services/AppStateService/AppStateService.swift +++ b/Sources/OversizeServices/Services/AppStateService/AppStateService.swift @@ -34,7 +34,7 @@ public final class AppStateService: ObservableObject { } appRunCount += 1 lastRunDate = Date() - lastRunVersion = Info.app.version.valueOrEmpty + lastRunVersion = Info.App.version.valueOrEmpty logDebugInfo() } diff --git a/Sources/OversizeServices/Services/SettingsService/SettingsService.swift b/Sources/OversizeServices/Services/SettingsService/SettingsService.swift index 4357d6c..903fc3e 100644 --- a/Sources/OversizeServices/Services/SettingsService/SettingsService.swift +++ b/Sources/OversizeServices/Services/SettingsService/SettingsService.swift @@ -20,25 +20,14 @@ public protocol SettingsServiceProtocol { var cloudKitCVVEnabled: Bool { get set } var healthKitEnabled: Bool { get set } var biometricEnabled: Bool { get } - var biometricWhenGetCVVEnabled: Bool { get } var pinCodeEnabled: Bool { get } - var deleteDataIfBruteForceEnabled: Bool { get set } - var spotlightEnabled: Bool { get set } - var alertPINCodeEnabled: Bool { get set } - var alertPINCode: String { get set } - var photoBreakerEnabled: Bool { get set } - var lockScreenDownEnabled: Bool { get set } var blurMinimizeEnabled: Bool { get set } - var authHistoryEnabled: Bool { get set } - var askPasswordWhenInactiveEnabled: Bool { get set } - var askPasswordAfterMinimizeEnabled: Bool { get set } var appLockTimeout: TimeInterval { get set } func getPINCode() -> String func setPINCode(pin: String) func updatePINCode(oldPIN: String, newPIN: String) async -> Bool func isSetPinCode() -> Bool func biometricChange(_ newState: Bool) async - func biometricWhenGetCVVChange(_ newState: Bool) async } public final class SettingsService: ObservableObject, SettingsServiceProtocol, @unchecked Sendable { @@ -57,19 +46,9 @@ public final class SettingsService: ObservableObject, SettingsServiceProtocol, @ // Security public static let biometricEnabled = "SettingsStore.biometricEnabled" - public static let biometricWhenGetCVVEnabend = "SettingsStore.faceIdWhenGetCVVEnabend" public static let pinCodeEnabend = "SettingsStore.pinCodeEnabend" public static let pinCode = "SettingsStore.pinCode" - public static let bruteForceSecurityEnabled = "SettingsStore.bruteForceSecurityEnabled" - public static let spotlightEnabled = "SettingsStore.spotlightEnabled" - public static let alertPINCodeEnabled = "SettingsStore.alertPINCodeEnabled" - public static let alertPINCode = "SettingsStore.alertPINCode" - public static let photoBreakerEnabend = "SettingsStore.photoBreakerEnabend" - public static let facedownLockEnabend = "SettingsStore.facedownLockEnabend" public static let blurMinimizeEnabend = "SettingsStore.blurWhenMinimizeEnabend" - public static let authHistoryEnabend = "SettingsStore.authHistoryEnabend" - public static let askPasswordWhenInactiveEnabend = "SettingsStore.askPasswordWhenInactiveEnabend" - public static let askPasswordAfterMinimizeEnabend = "SettingsStore.askPasswordAfterMinimizeEnabend" public static let appLockTimeout = "SettingsStore.TimeToLock" public static let fastEnter = "SettingsStore.FastEnter" } @@ -84,70 +63,11 @@ public final class SettingsService: ObservableObject, SettingsServiceProtocol, @ // Security @AppStorage(Keys.biometricEnabled) public var biometricEnabled = false - @AppStorage(Keys.biometricWhenGetCVVEnabend) public var biometricWhenGetCVVEnabled = false @AppStorage(Keys.pinCodeEnabend) public var pinCodeEnabled = false - @AppStorage(Keys.bruteForceSecurityEnabled) public var deleteDataIfBruteForceEnabled = false - @AppStorage(Keys.spotlightEnabled) public var spotlightEnabled = false - @AppStorage(Keys.alertPINCodeEnabled) public var alertPINCodeEnabled = false - @AppStorage(Keys.alertPINCode) public var alertPINCode = "" - @AppStorage(Keys.photoBreakerEnabend) public var photoBreakerEnabled = false - @AppStorage(Keys.facedownLockEnabend) public var lockScreenDownEnabled = false @AppStorage(Keys.blurMinimizeEnabend) public var blurMinimizeEnabled = false - @AppStorage(Keys.authHistoryEnabend) public var authHistoryEnabled = false - @AppStorage(Keys.askPasswordWhenInactiveEnabend) public var askPasswordWhenInactiveEnabled = false - @AppStorage(Keys.askPasswordAfterMinimizeEnabend) public var askPasswordAfterMinimizeEnabled = false @AppStorage(Keys.appLockTimeout) public var appLockTimeout: TimeInterval = .init(60.0) @AppStorage(Keys.fastEnter) public var fastEnter: Bool = false @SecureStorage(Keys.pinCode) private var pinCode - - // Deprecated property aliases for backward compatibility - @available(*, deprecated, message: "Use biometricWhenGetCVVEnabled instead") - public var biometricWhenGetCVVEnabend: Bool { - get { biometricWhenGetCVVEnabled } - set { biometricWhenGetCVVEnabled = newValue } - } - - @available(*, deprecated, message: "Use pinCodeEnabled instead") - public var pinCodeEnabend: Bool { - get { pinCodeEnabled } - set { pinCodeEnabled = newValue } - } - - @available(*, deprecated, message: "Use photoBreakerEnabled instead") - public var photoBreakerEnabend: Bool { - get { photoBreakerEnabled } - set { photoBreakerEnabled = newValue } - } - - @available(*, deprecated, message: "Use lockScreenDownEnabled instead") - public var lookScreenDownEnabend: Bool { - get { lockScreenDownEnabled } - set { lockScreenDownEnabled = newValue } - } - - @available(*, deprecated, message: "Use blurMinimizeEnabled instead") - public var blurMinimizeEnabend: Bool { - get { blurMinimizeEnabled } - set { blurMinimizeEnabled = newValue } - } - - @available(*, deprecated, message: "Use authHistoryEnabled instead") - public var authHistoryEnabend: Bool { - get { authHistoryEnabled } - set { authHistoryEnabled = newValue } - } - - @available(*, deprecated, message: "Use askPasswordWhenInactiveEnabled instead") - public var askPasswordWhenInactiveEnabend: Bool { - get { askPasswordWhenInactiveEnabled } - set { askPasswordWhenInactiveEnabled = newValue } - } - - @available(*, deprecated, message: "Use askPasswordAfterMinimizeEnabled instead") - public var askPasswordAfterMinimizeEnabend: Bool { - get { askPasswordAfterMinimizeEnabled } - set { askPasswordAfterMinimizeEnabled = newValue } - } } // PIN Code @@ -208,18 +128,4 @@ public extension SettingsService { biometricEnabled = newState } } - - func biometricWhenGetCVVChange(_ newState: Bool) async { - var reason = "" - let biometricType = biometricService.biometricType - if newState { - reason = "Provide \(biometricType.rawValue) to enable" - } else { - reason = "Provide \(biometricType.rawValue) to disable" - } - let auth = await biometricService.authenticating(reason: reason) - if auth { - biometricWhenGetCVVEnabled = newState - } - } } From 557490fb7c491172931acfccad3db994a815544f Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Thu, 19 Feb 2026 12:22:11 +0300 Subject: [PATCH 4/5] Migrate from AppError to Error --- Package.swift | 17 +- README.md | 105 ++++++++ .../CalendarService.swift | 31 ++- .../Extensions/EKEventExtension.swift | 3 +- .../ContactsService.swift | 12 +- .../CloudDocumentsService.swift | 33 ++- .../FileManagerService.swift | 29 ++- .../FileManagerSyncService.swift | 37 +-- .../BloodPressureService.swift | 97 ++++---- .../BodyMassService.swift | 10 +- .../HealthKitService.swift | 53 ++--- .../LocationService.swift | 12 +- .../LocalNotificationService.swift | 15 +- .../FeatureFlags/FeatureFlags.swift | 3 +- .../OversizeServices/Info/Deprecated.swift | 224 +++++++++++++----- .../SettingsService/SettingsService.swift | 2 +- .../Models/StoreSpecialOfferEventType.swift | 3 +- .../StoreKitService.swift | 20 +- .../OversizeServicesTests.swift | 2 +- 19 files changed, 449 insertions(+), 259 deletions(-) diff --git a/Package.swift b/Package.swift index 5403f2b..fc17835 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import Foundation @@ -7,15 +7,13 @@ import PackageDescription let remoteDependencies: [PackageDescription.Package.Dependency] = [ .package(url: "https://github.com/oversizedev/OversizeCore.git", .upToNextMajor(from: "1.3.0")), .package(url: "https://github.com/oversizedev/OversizeLocalizable.git", .upToNextMajor(from: "1.5.0")), - .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), - .package(url: "https://github.com/oversizedev/OversizeModels.git", .upToNextMajor(from: "0.1.0")), + .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.5.0")), ] let localDependencies: [PackageDescription.Package.Dependency] = [ .package(name: "OversizeCore", path: "../OversizeCore"), .package(name: "OversizeLocalizable", path: "../OversizeLocalizable"), - .package(name: "OversizeModels", path: "../OversizeModels"), - .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.1.3")), + .package(url: "https://github.com/hmlongco/Factory.git", .upToNextMajor(from: "2.5.0")), ] let dependencies: [PackageDescription.Package.Dependency] = remoteDependencies @@ -46,7 +44,6 @@ let package = Package( dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), .product(name: "OversizeLocalizable", package: "OversizeLocalizable"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -54,14 +51,13 @@ let package = Package( name: "OversizeFileManagerService", dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), .target( name: "OversizeContactsService", dependencies: [ - .product(name: "OversizeModels", package: "OversizeModels"), + .product(name: "OversizeCore", package: "OversizeCore"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -69,7 +65,6 @@ let package = Package( name: "OversizeCalendarService", dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -77,7 +72,6 @@ let package = Package( name: "OversizeHealthService", dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -85,7 +79,6 @@ let package = Package( name: "OversizeLocationService", dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -94,7 +87,6 @@ let package = Package( dependencies: [ "OversizeServices", .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), @@ -102,7 +94,6 @@ let package = Package( name: "OversizeNotificationService", dependencies: [ .product(name: "OversizeCore", package: "OversizeCore"), - .product(name: "OversizeModels", package: "OversizeModels"), .product(name: "FactoryKit", package: "Factory"), ], ), diff --git a/README.md b/README.md index a317d22..49b73bc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ A comprehensive collection of service modules for Apple platforms that provides - 👥 **Contacts Services** - Contacts framework integration - 🔔 **Notification Services** - Local notifications management - 📁 **File Manager Services** - File operations with iCloud Documents support +- 🌐 **Translation Services** - Apple Translation framework integration for text translation +- 🧠 **Intelligence Services** - Apple Intelligence framework integration (iOS 26.0+) - 🏭 **Dependency Injection** - Factory-based service registration and injection - 🌐 **Multi-platform** - Support for iOS, macOS, tvOS, and watchOS @@ -358,6 +360,109 @@ let localURL = URL(fileURLWithPath: "/path/to/local/document.pdf") let localResult = await fileManager.saveDocument(pickedURL: localURL, folder: "LocalDocs") ``` +### 🌐 TranslationService + +Apple Translation framework integration for text translation. + +**Features:** +- Single and batch text translation +- Automatic source language detection +- Streaming batch translations +- Pre-loading translation models +- Multi-platform support (iOS 26.0+, macOS 26.0+) + +**Usage Example:** + +```swift +import OversizeServices +import FactoryKit + +// Inject the service +@Injected(\.translationService) var translationService: TranslationServiceProtocol + +// Single translation +do { + let translation = try await translationService.translate( + "Hello, world!", + from: .init(languageCode: .english), + to: .init(languageCode: .russian) + ) + print("Translation: \(translation)") +} catch { + print("Translation error: \(error)") +} + +// Auto-detect source language +let autoTranslation = try await translationService.translate( + "Hello, world!", + from: nil, // auto-detect + to: .init(languageCode: .russian) +) + +// Batch translation +let texts = ["Hello", "Goodbye", "Thank you"] +let translations = try await translationService.translate( + texts, + from: .init(languageCode: .english), + to: .init(languageCode: .russian) +) + +// Streaming batch translation +let requests = texts.enumerated().map { + (text: $0.element, id: "\($0.offset)") +} + +for try await (id, translation) in translationService.translateBatch( + requests, + from: nil, + to: .init(languageCode: .russian) +) { + print("[\(id)] \(translation)") +} + +// Pre-load translation model +try await translationService.prepareTranslation( + from: .init(languageCode: .english), + to: .init(languageCode: .russian) +) +``` + +**Platform Requirements:** +- Protocol available from iOS 17.4+, macOS 14.4+ (for dependency injection) +- Translation functionality requires iOS 26.0+, macOS 26.0+ +- Requires physical device (does not work in Simulator) +- Internet connection needed for initial language model download + +### 🧠 IntelligenceService + +Apple Intelligence framework integration (iOS 26.0+, macOS 26.0+). + +**Features:** +- Text summarization +- Writing tools integration +- Privacy-focused on-device processing + +**Usage Example:** + +```swift +import OversizeServices +import FactoryKit + +// Inject the service (iOS 26.0+ only) +@Injected(\.intelligenceService) var intelligenceService: IntelligenceServiceProtocol + +// Summarize text +do { + let summary = try await intelligenceService.summarize( + "Long text to summarize...", + type: .brief + ) + print("Summary: \(summary)") +} catch { + print("Summarization error: \(error)") +} +``` + ### 🏭 OversizeServices (Core) The main module that provides service registration and dependency injection setup. diff --git a/Sources/OversizeCalendarService/CalendarService.swift b/Sources/OversizeCalendarService/CalendarService.swift index a9b7907..1a41c85 100644 --- a/Sources/OversizeCalendarService/CalendarService.swift +++ b/Sources/OversizeCalendarService/CalendarService.swift @@ -8,14 +8,13 @@ #endif import Foundation import OversizeCore -import OversizeModels #if !os(tvOS) public class CalendarService: @unchecked Sendable { private let eventStore: EKEventStore = .init() public init() {} - func requestFullAccess() async -> Result { + func requestFullAccess() async -> Result { do { let status: Bool = if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) { try await eventStore.requestFullAccessToEvents() @@ -25,14 +24,14 @@ public class CalendarService: @unchecked Sendable { if status { return .success(true) } else { - return .failure(AppError.eventKit(type: .notAccess)) + return .failure(CalendarError.accessDenied) } } catch { - return .failure(AppError.eventKit(type: .notAccess)) + return .failure(CalendarError.accessDenied) } } - func requestWriteOnlyAccess() async -> Result { + func requestWriteOnlyAccess() async -> Result { do { let status: Bool = if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) { try await eventStore.requestWriteOnlyAccessToEvents() @@ -42,14 +41,14 @@ public class CalendarService: @unchecked Sendable { if status { return .success(true) } else { - return .failure(AppError.eventKit(type: .notAccess)) + return .failure(CalendarError.accessDenied) } } catch { - return .failure(AppError.eventKit(type: .notAccess)) + return .failure(CalendarError.accessDenied) } } - public func fetchEvents(start: Date, end: Date = Date(), filtredCalendarsIds: [String] = []) async -> Result<[EKEvent], AppError> { + public func fetchEvents(start: Date, end: Date = Date(), filtredCalendarsIds: [String] = []) async -> Result<[EKEvent], Error> { let access = await requestFullAccess() if case let .failure(error) = access { return .failure(error) } let calendars = eventStore.calendars(for: .event) @@ -70,21 +69,21 @@ public class CalendarService: @unchecked Sendable { return .success(events) } - public func fetchCalendars() async -> Result<[EKCalendar], AppError> { + public func fetchCalendars() async -> Result<[EKCalendar], Error> { let access = await requestFullAccess() if case let .failure(error) = access { return .failure(error) } let calendars = eventStore.calendars(for: .event) return .success(calendars) } - public func fetchDefaultCalendar() async -> Result { + public func fetchDefaultCalendar() async -> Result { let access = await requestFullAccess() if case let .failure(error) = access { return .failure(error) } let calendar = eventStore.defaultCalendarForNewEvents return .success(calendar) } - public func fetchSourses() async -> Result<[EKSource], AppError> { + public func fetchSourses() async -> Result<[EKSource], Error> { let access = await requestFullAccess() if case let .failure(error) = access { return .failure(error) } let calendars = eventStore.sources @@ -108,7 +107,7 @@ public class CalendarService: @unchecked Sendable { recurrenceRules: CalendarEventRecurrenceRules = .never, recurrenceEndRules: CalendarEventEndRecurrenceRules = .never, span: EKSpan = .thisEvent, - ) async -> Result { + ) async -> Result { let access = await requestWriteOnlyAccess() if case let .failure(error) = access { return .failure(error) } let newEvent: EKEvent = if let event { @@ -159,15 +158,15 @@ public class CalendarService: @unchecked Sendable { #endif return .success(true) } catch { - return .failure(.eventKit(type: .savingItem)) + return .failure(CalendarError.saveFailed) } } @available(iOS 15.0, macOS 13.0, visionOS 1.0, *) - public func deleteEvent(identifier: String, span: EKSpan = .thisEvent) async -> Result { + public func deleteEvent(identifier: String, span: EKSpan = .thisEvent) async -> Result { let access = await requestFullAccess() if case let .failure(error) = access { return .failure(error) } - guard let event = eventStore.fetchEvent(identifier: identifier) else { return .failure(.custom(title: "Not deleted")) } + guard let event = eventStore.fetchEvent(identifier: identifier) else { return .failure(CalendarError.itemNotFound) } do { #if !os(watchOS) @@ -175,7 +174,7 @@ public class CalendarService: @unchecked Sendable { #endif return .success(true) } catch { - return .failure(.eventKit(type: .deleteItem)) + return .failure(CalendarError.deleteFailed) } } } diff --git a/Sources/OversizeCalendarService/Extensions/EKEventExtension.swift b/Sources/OversizeCalendarService/Extensions/EKEventExtension.swift index 4413fb1..2edff3d 100644 --- a/Sources/OversizeCalendarService/Extensions/EKEventExtension.swift +++ b/Sources/OversizeCalendarService/Extensions/EKEventExtension.swift @@ -22,8 +22,7 @@ public extension EKEvent { return meetType.title } else if let location = location?.components(separatedBy: .newlines), let locationText: String = location.first { if locationText.count < 16 { - let clean = locationText.trimmingCharacters(in: .whitespacesAndNewlines) - return clean + return locationText.trimmingCharacters(in: .whitespacesAndNewlines) } else { var clean = locationText.trimmingCharacters(in: .whitespacesAndNewlines) let range = clean.index(clean.startIndex, offsetBy: 16) ..< clean.endIndex diff --git a/Sources/OversizeContactsService/ContactsService.swift b/Sources/OversizeContactsService/ContactsService.swift index 2213380..1270992 100644 --- a/Sources/OversizeContactsService/ContactsService.swift +++ b/Sources/OversizeContactsService/ContactsService.swift @@ -7,23 +7,23 @@ import Contacts #endif import Foundation -import OversizeModels +import OversizeCore #if canImport(Contacts) public class ContactsService: @unchecked Sendable { private let contactStore: CNContactStore = .init() public init() {} - public func requestAccess() async -> Result { + public func requestAccess() async -> Result { do { let status = try await contactStore.requestAccess(for: .contacts) if status { return .success(true) } else { - return .failure(AppError.contacts(type: .notAccess)) + return .failure(ContactsError.notAccess) } } catch { - return .failure(AppError.contacts(type: .notAccess)) + return .failure(ContactsError.notAccess) } } @@ -31,7 +31,7 @@ public class ContactsService: @unchecked Sendable { keysToFetch: [CNKeyDescriptor] = [CNContactVCardSerialization.descriptorForRequiredKeys()], order: CNContactSortOrder = .none, unifyResults: Bool = true, - ) async -> Result<[CNContact], AppError> { + ) async -> Result<[CNContact], Error> { var contacts: [CNContact] = [] let fetchRequest = CNContactFetchRequest(keysToFetch: keysToFetch) fetchRequest.unifyResults = unifyResults @@ -42,7 +42,7 @@ public class ContactsService: @unchecked Sendable { } return .success(contacts) } catch { - return .failure(AppError.contacts(type: .unknown)) + return .failure(ContactsError.unknown(error)) } } } diff --git a/Sources/OversizeFileManagerService/CloudDocumentsService.swift b/Sources/OversizeFileManagerService/CloudDocumentsService.swift index 7ba6982..1756f14 100644 --- a/Sources/OversizeFileManagerService/CloudDocumentsService.swift +++ b/Sources/OversizeFileManagerService/CloudDocumentsService.swift @@ -5,23 +5,22 @@ import Foundation import OversizeCore -import OversizeModels public protocol CloudDocumentsServiceProtocol { - func saveDocument(localDocumentsURL: URL, folder: String?, containerId: String?) async -> Result - func removeDocument(icloudUrl: URL) async -> Result - func removeFolder(_ folder: String, containerId: String?) async -> Result + func saveDocument(localDocumentsURL: URL, folder: String?, containerId: String?) async -> Result + func removeDocument(icloudUrl: URL) async -> Result + func removeFolder(_ folder: String, containerId: String?) async -> Result func giveURL(folder: String?, file: String, containerId: String?) async -> URL? } public final class CloudDocumentsService: CloudDocumentsServiceProtocol { - public func saveDocument(localDocumentsURL: URL, folder: String?, containerId: String? = nil) async -> Result { + public func saveDocument(localDocumentsURL: URL, folder: String?, containerId: String? = nil) async -> Result { if let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId)?.appendingPathComponent("Documents", isDirectory: true) { if !FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: nil) { do { try FileManager.default.createDirectory(at: iCloudDocumentsURL, withIntermediateDirectories: true, attributes: nil) } catch { - return .failure(.cloudDocuments(type: .savingItem)) + return .failure(CloudError.saveFailed) } } } @@ -37,7 +36,7 @@ public final class CloudDocumentsService: CloudDocumentsServiceProtocol { do { try FileManager.default.createDirectory(at: iCloudDocumentsFolderURL, withIntermediateDirectories: true, attributes: nil) } catch { - return .failure(.cloudDocuments(type: .savingItem)) + return .failure(CloudError.saveFailed) } } } @@ -54,19 +53,19 @@ public final class CloudDocumentsService: CloudDocumentsServiceProtocol { var isDir: ObjCBool = false guard let iCloudDocumentsURL else { - return .failure(.cloudDocuments(type: .notAccess)) + return .failure(CloudError.accessDenied) } if FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: &isDir) { do { try FileManager.default.removeItem(at: iCloudDocumentsURL) } catch { - return .failure(.cloudDocuments(type: .savingItem)) + return .failure(CloudError.saveFailed) } } do { - guard localDocumentsURL.startAccessingSecurityScopedResource() else { return .failure(.cloudKit(type: .notAccess)) } + guard localDocumentsURL.startAccessingSecurityScopedResource() else { return .failure(CloudError.accessDenied) } defer { localDocumentsURL.stopAccessingSecurityScopedResource() } @@ -74,24 +73,24 @@ public final class CloudDocumentsService: CloudDocumentsServiceProtocol { try FileManager.default.copyItem(at: localDocumentsURL, to: iCloudDocumentsURL) return .success(iCloudDocumentsURL) } catch { - return .failure(.cloudDocuments(type: .savingItem)) + return .failure(CloudError.saveFailed) } } - public func removeDocument(icloudUrl: URL) async -> Result { + public func removeDocument(icloudUrl: URL) async -> Result { do { try FileManager.default.removeItem(at: icloudUrl) return .success(true) } catch { - return .failure(.cloudDocuments(type: .deleteItem)) + return .failure(CloudError.deleteFailed) } } - public func removeFolder(_ folder: String, containerId: String? = nil) async -> Result { + public func removeFolder(_ folder: String, containerId: String? = nil) async -> Result { guard let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId)? .appendingPathComponent("Documents", isDirectory: true) .appendingPathComponent(folder, isDirectory: true) - else { return .failure(.cloudDocuments(type: .deleteItem)) } + else { return .failure(CloudError.deleteFailed) } var isDir: ObjCBool = true if FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: &isDir) { @@ -99,10 +98,10 @@ public final class CloudDocumentsService: CloudDocumentsServiceProtocol { try FileManager.default.removeItem(at: iCloudDocumentsURL) return .success(true) } catch { - return .failure(.cloudDocuments(type: .deleteItem)) + return .failure(CloudError.deleteFailed) } } - return .failure(.cloudDocuments(type: .deleteItem)) + return .failure(CloudError.deleteFailed) } public func giveURL(folder: String?, file: String, containerId: String? = nil) -> URL? { diff --git a/Sources/OversizeFileManagerService/FileManagerService.swift b/Sources/OversizeFileManagerService/FileManagerService.swift index d540894..b0bb3d7 100644 --- a/Sources/OversizeFileManagerService/FileManagerService.swift +++ b/Sources/OversizeFileManagerService/FileManagerService.swift @@ -5,12 +5,11 @@ import Foundation import OversizeCore -import OversizeModels public protocol FileManagerServiceProtocol { - func saveDocument(pickedURL: URL, folder: String?) async -> Result - func removeDocument(localURL: URL) async -> Result - func removeFolder(_ folder: String) async -> Result + func saveDocument(pickedURL: URL, folder: String?) async -> Result + func removeDocument(localURL: URL) async -> Result + func removeFolder(_ folder: String) async -> Result func giveURL(folder: String?, file: String) async -> URL? } @@ -30,9 +29,9 @@ public final class FileManagerService: FileManagerServiceProtocol { /// - pickedURL: URL of picked document /// - project: project name /// - Returns: Local URL of document - public func saveDocument(pickedURL: URL, folder: String?) async -> Result { + public func saveDocument(pickedURL: URL, folder: String?) async -> Result { guard let url = rootUrl else { - return .failure(.fileManager(type: .notAccess)) + return .failure(FileError.accessDenied) } do { var destinationDocumentsURL: URL = url @@ -50,7 +49,7 @@ public final class FileManagerService: FileManagerServiceProtocol { do { try FileManager.default.createDirectory(at: destinationDocumentsURL, withIntermediateDirectories: true, attributes: nil) } catch { - return .failure(.fileManager(type: .savingItem)) + return .failure(FileError.saveFailed) } } @@ -59,7 +58,7 @@ public final class FileManagerService: FileManagerServiceProtocol { try FileManager.default.removeItem(at: destinationDocumentsURL) } guard pickedURL.startAccessingSecurityScopedResource() else { - return .failure(.fileManager(type: .savingItem)) + return .failure(FileError.saveFailed) } defer { @@ -68,18 +67,18 @@ public final class FileManagerService: FileManagerServiceProtocol { try FileManager.default.copyItem(at: pickedURL, to: destinationDocumentsURL) return .success(destinationDocumentsURL) } catch { - return .failure(.fileManager(type: .savingItem)) + return .failure(FileError.saveFailed) } } /// Remove Document at URL /// - Parameter localURL: URL of Document to be removed - public func removeDocument(localURL: URL) async -> Result { + public func removeDocument(localURL: URL) async -> Result { do { try FileManager.default.removeItem(at: localURL) return .success(true) } catch { - return .failure(.fileManager(type: .deleteItem)) + return .failure(FileError.deleteFailed) } } @@ -108,9 +107,9 @@ public final class FileManagerService: FileManagerServiceProtocol { /// Remove Project Directory /// - Parameter project: project name - public func removeFolder(_ folder: String) async -> Result { + public func removeFolder(_ folder: String) async -> Result { guard let url = rootUrl else { - return .failure(.fileManager(type: .deleteItem)) + return .failure(FileError.deleteFailed) } let localDocumentsURL = url .appendingPathComponent(folder, isDirectory: true) @@ -121,9 +120,9 @@ public final class FileManagerService: FileManagerServiceProtocol { try FileManager.default.removeItem(at: localDocumentsURL) return .success(true) } catch { - return .failure(.fileManager(type: .deleteItem)) + return .failure(FileError.deleteFailed) } } - return .failure(.fileManager(type: .deleteItem)) + return .failure(FileError.deleteFailed) } } diff --git a/Sources/OversizeFileManagerService/FileManagerSyncService.swift b/Sources/OversizeFileManagerService/FileManagerSyncService.swift index 1a7b984..ebaed98 100644 --- a/Sources/OversizeFileManagerService/FileManagerSyncService.swift +++ b/Sources/OversizeFileManagerService/FileManagerSyncService.swift @@ -6,13 +6,12 @@ import FactoryKit import Foundation import OversizeCore -import OversizeModels public protocol FileManagerSyncServiceProtocol { - func isICloudContainerAvailable() -> Result - func generateUrl(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result - func deleteDocument(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result - func saveDocument(fileURL: URL, folder: String?, location: FileLocation) async -> Result + func isICloudContainerAvailable() -> Result + func generateUrl(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result + func deleteDocument(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result + func saveDocument(fileURL: URL, folder: String?, location: FileLocation) async -> Result } public class FileManagerSyncService { @@ -29,18 +28,17 @@ extension FileManagerSyncService: FileManagerSyncServiceProtocol { } } - public func isICloudContainerAvailable() -> Result { + public func isICloudContainerAvailable() -> Result { if FileManager.default.ubiquityIdentityToken != nil { - .success(true) - } else { - .failure(.cloudDocuments(type: .notAccess)) + return .success(true) } + return .failure(CloudError.noAccount) } - public func generateUrl(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result { + public func generateUrl(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result { if URL(string: urlString.valueOrEmpty)?.fileExists() ?? false { guard let url = URL(string: urlString.valueOrEmpty) else { - return .failure(.cloudDocuments(type: .fetchItems)) + return .failure(FileError.fetchFailed) } return .success(url) } @@ -49,24 +47,29 @@ extension FileManagerSyncService: FileManagerSyncServiceProtocol { do { try await downloadFromCloud(url: url) } catch { - return .failure(.cloudDocuments(type: .fetchItems)) + return .failure(CloudError.fetchFailed) } } else { let url = await fileManagerService.giveURL(folder: folder, file: file ?? "file") if url?.fileExists() ?? false { - guard let url else { return .failure(.fileManager(type: .fetchItems)) } + guard let url else { return .failure(FileError.fetchFailed) } return .success(url) } } - return .failure(.cloudDocuments(type: .fetchItems)) + switch location { + case .iCloud: + return .failure(CloudError.fetchFailed) + case .local: + return .failure(FileError.fetchFailed) + } } - public func deleteDocument(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result { + public func deleteDocument(urlString: String?, location: FileLocation, folder: String?, file: String?) async -> Result { let result = await generateUrl(urlString: urlString, location: location, folder: folder, file: file) switch result { case let .success(url): if location == .local, !url.fileExists() { - return .failure(.fileManager(type: .deleteItem)) + return .failure(FileError.deleteFailed) } let status = isICloudContainerAvailable() switch status { @@ -80,7 +83,7 @@ extension FileManagerSyncService: FileManagerSyncServiceProtocol { } } - public func saveDocument(fileURL: URL, folder: String?, location: FileLocation) async -> Result { + public func saveDocument(fileURL: URL, folder: String?, location: FileLocation) async -> Result { switch location { case .iCloud: let status = isICloudContainerAvailable() diff --git a/Sources/OversizeHealthService/BloodPressureService.swift b/Sources/OversizeHealthService/BloodPressureService.swift index 78c65fa..a721a61 100644 --- a/Sources/OversizeHealthService/BloodPressureService.swift +++ b/Sources/OversizeHealthService/BloodPressureService.swift @@ -8,7 +8,6 @@ import Foundation import HealthKit #endif import OversizeCore -import OversizeModels #if os(iOS) || os(macOS) @available(iOS 15, macOS 13.0, *) @@ -21,12 +20,12 @@ public class BloodPressureService: HealthKitService, @unchecked Sendable { @available(iOS 15, macOS 13.0, *) public extension BloodPressureService { - func requestAuthorization() async -> Result { - guard let healthStore, - let bloodPressureSystolicType, + func requestAuthorization() async -> Result { + guard let healthStore else { return .failure(HealthError.accessDenied) } + guard let bloodPressureSystolicType, let bloodPressureDiastolicType, let heartRateType - else { return .failure(AppError.healthKit(type: .unknown)) } + else { return .failure(HealthError.dataTypeNotAvailable) } do { try await healthStore.requestAuthorization( @@ -43,16 +42,16 @@ public extension BloodPressureService { ) return .success(true) } catch { - return .failure(AppError.healthKit(type: .notAccess)) + return .failure(HealthError.unknown(error)) } } } -// Work with SyncId +/// Work with SyncId @available(iOS 15, macOS 13.0, *) public extension BloodPressureService { - func saveHeartRate(value: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { - guard let heartRateType else { return .failure(AppError.healthKit(type: .savingItem)) } + func saveHeartRate(value: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { + guard let heartRateType else { return .failure(HealthError.saveFailed) } var metadata = [String: Any]() metadata[HKMetadataKeySyncIdentifier] = syncId.uuidString metadata[HKMetadataKeySyncVersion] = syncVersion @@ -62,11 +61,11 @@ public extension BloodPressureService { return await saveQuantitySample(heartRateSample) } - func saveBloodPressure(systolic: Int, diastolic: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { + func saveBloodPressure(systolic: Int, diastolic: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType - else { return .failure(AppError.healthKit(type: .savingItem)) } + else { return .failure(HealthError.saveFailed) } var metadata = [String: Any]() metadata[HKMetadataKeySyncIdentifier] = syncId.uuidString @@ -86,13 +85,13 @@ public extension BloodPressureService { return await saveCorrelation(correlation) } - func saveBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result<(HKCorrelation, HKQuantitySample), AppError> { + func saveBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result<(HKCorrelation, HKQuantitySample), Error> { guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType, let heartRateType else { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } var metadata = [String: Any]() @@ -123,30 +122,30 @@ public extension BloodPressureService { } } - func deleteBloodPressure(syncId: UUID) async -> Result { - guard let bloodPressureType else { return .failure(AppError.healthKit(type: .deleteItem)) } + func deleteBloodPressure(syncId: UUID) async -> Result { + guard let bloodPressureType else { return .failure(HealthError.deleteFailed) } return await delete(type: bloodPressureType, syncId: syncId) } - func deleteHeartRate(syncId: UUID) async -> Result { - guard let heartRateType else { return .failure(AppError.healthKit(type: .deleteItem)) } + func deleteHeartRate(syncId: UUID) async -> Result { + guard let heartRateType else { return .failure(HealthError.deleteFailed) } return await delete(type: heartRateType, syncId: syncId) } - func replaceBloodPressure(systolic: Int, diastolic: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { + func replaceBloodPressure(systolic: Int, diastolic: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { await saveBloodPressure(systolic: systolic, diastolic: diastolic, date: date, syncId: syncId, syncVersion: syncVersion + 1) } - func replaceBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result<(HKCorrelation, HKQuantitySample), AppError> { + func replaceBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result<(HKCorrelation, HKQuantitySample), Error> { await saveBloodPressure(systolic: systolic, diastolic: diastolic, heartRate: heartRate, date: date, syncId: syncId, syncVersion: syncVersion + 1) } - func replaceHeartRate(value: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { + func replaceHeartRate(value: Int, date: Date, syncId: UUID, syncVersion: Int) async -> Result { await saveHeartRate(value: value, date: date, syncId: syncId, syncVersion: syncVersion + 1) } - func fetchBloodPressure(startDate: Date, endDate: Date = Date()) async -> Result<[(UUID, UUID?, Double, Double, Date)], AppError> { - guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType else { return .failure(AppError.healthKit(type: .fetchItems)) } + func fetchBloodPressure(startDate: Date, endDate: Date = Date()) async -> Result<[(UUID, UUID?, Double, Double, Date)], Error> { + guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType else { return .failure(HealthError.fetchFailed) } let result = await fetchCorrelation(startDate: startDate, endDate: endDate, type: bloodPressureType) switch result { case let .success(data): @@ -182,11 +181,11 @@ public extension BloodPressureService { } } -// Work with HKObjects UUIDs +/// Work with HKObjects UUIDs @available(iOS 15, macOS 13.0, *) public extension BloodPressureService { - func fetchBloodPressure(startDate: Date, endDate: Date = Date()) async -> Result<[(UUID, Double, Double, Date)], AppError> { - guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType else { return .failure(AppError.healthKit(type: .fetchItems)) } + func fetchBloodPressure(startDate: Date, endDate: Date = Date()) async -> Result<[(UUID, Double, Double, Date)], Error> { + guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType else { return .failure(HealthError.fetchFailed) } let result = await fetchCorrelation(startDate: startDate, endDate: endDate, type: bloodPressureType) switch result { case let .success(data): @@ -209,12 +208,12 @@ public extension BloodPressureService { } } - func fetchBloodPressureObjectById(uuid: UUID) async -> Result { - guard let bloodPressureType else { return .failure(AppError.healthKit(type: .fetchItems)) } + func fetchBloodPressureObjectById(uuid: UUID) async -> Result { + guard let bloodPressureType else { return .failure(HealthError.fetchFailed) } return await fetchCorrelationById(uuid: uuid, type: bloodPressureType) } - func fetcHeartRate(startDate: Date, endDate: Date = Date()) async throws -> Result<[(UUID, Double, Date)], AppError> { + func fetcHeartRate(startDate: Date, endDate: Date = Date()) async throws -> Result<[(UUID, Double, Date)], Error> { let result = await fetchHeartRateSample(startDate: startDate, endDate: endDate) switch result { case let .success(samples): @@ -225,17 +224,17 @@ public extension BloodPressureService { } } - func fetchHeartRateSample(startDate: Date, endDate: Date = Date()) async -> Result<[HKQuantitySample], AppError> { - guard let heartRateType else { return .failure(.healthKit(type: .fetchItems)) } + func fetchHeartRateSample(startDate: Date, endDate: Date = Date()) async -> Result<[HKQuantitySample], Error> { + guard let heartRateType else { return .failure(HealthError.fetchFailed) } return await fetchHKQuantitySample(startDate: startDate, endDate: endDate, type: heartRateType) } - func fetchHeartRateObjectById(uuid: UUID) async -> Result { - guard let heartRateType else { return .failure(AppError.healthKit(type: .fetchItems)) } + func fetchHeartRateObjectById(uuid: UUID) async -> Result { + guard let heartRateType else { return .failure(HealthError.fetchFailed) } return await fetchObjectById(uuid: uuid, type: heartRateType) } - func replaceBloodPressure(uuid: UUID, systolic: Int, diastolic: Int, date: Date) async -> Result { + func replaceBloodPressure(uuid: UUID, systolic: Int, diastolic: Int, date: Date) async -> Result { let result = await fetchBloodPressureObjectById(uuid: uuid) switch result { case let .success(object): @@ -244,14 +243,14 @@ public extension BloodPressureService { case .success: return await saveBloodPressure(systolic: systolic, diastolic: diastolic, date: date) case .failure: - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } case .failure: - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } } - func replaceBloodPressure(bpUuid: UUID, hrUuid: UUID, systolic: Int, diastolic: Int, heartRate: Int, date: Date) async -> Result<(HKCorrelation, HKQuantitySample), AppError> { + func replaceBloodPressure(bpUuid: UUID, hrUuid: UUID, systolic: Int, diastolic: Int, heartRate: Int, date: Date) async -> Result<(HKCorrelation, HKQuantitySample), Error> { let resultBloodPressureObject = await fetchBloodPressureObjectById(uuid: bpUuid) let resultHeartRateObject = await fetchHeartRateObjectById(uuid: hrUuid) if case let .success(bloodPressureObject) = resultBloodPressureObject, case let .success(heartRateObject) = resultHeartRateObject { @@ -260,14 +259,14 @@ public extension BloodPressureService { case .success: return await saveBloodPressure(systolic: systolic, diastolic: diastolic, heartRate: heartRate, date: date) case .failure: - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } } else { - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } } - func deleteBloodPressure(uuid: UUID) async -> Result { + func deleteBloodPressure(uuid: UUID) async -> Result { let result = await fetchBloodPressureObjectById(uuid: uuid) switch result { case let .success(object): @@ -277,11 +276,11 @@ public extension BloodPressureService { } } - func saveBloodPressure(systolic: Int, diastolic: Int, date: Date = Date()) async -> Result { + func saveBloodPressure(systolic: Int, diastolic: Int, date: Date = Date()) async -> Result { guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType - else { return .failure(AppError.healthKit(type: .savingItem)) } + else { return .failure(HealthError.saveFailed) } let unit = HKUnit.millimeterOfMercury() @@ -297,13 +296,13 @@ public extension BloodPressureService { return await saveCorrelation(correlation) } - func saveBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date = Date()) async -> Result<(HKCorrelation, HKQuantitySample), AppError> { + func saveBloodPressure(systolic: Int, diastolic: Int, heartRate: Int, date: Date = Date()) async -> Result<(HKCorrelation, HKQuantitySample), Error> { guard let bloodPressureType, let bloodPressureSystolicType, let bloodPressureDiastolicType, let heartRateType else { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } let unit = HKUnit.millimeterOfMercury() @@ -330,15 +329,15 @@ public extension BloodPressureService { } } - func saveHeartRate(value: Int, date: Date) async -> Result { - guard let heartRateType else { return .failure(AppError.healthKit(type: .savingItem)) } + func saveHeartRate(value: Int, date: Date) async -> Result { + guard let heartRateType else { return .failure(HealthError.saveFailed) } let beatsCountUnit = HKUnit.count() let heartRateQuantity = HKQuantity(unit: beatsCountUnit.unitDivided(by: HKUnit.minute()), doubleValue: Double(value)) let heartRateSample = HKQuantitySample(type: heartRateType, quantity: heartRateQuantity, start: date, end: date) return await saveQuantitySample(heartRateSample) } - func deleteHeartRate(uuid: UUID) async -> Result { + func deleteHeartRate(uuid: UUID) async -> Result { let result = await fetchHeartRateObjectById(uuid: uuid) switch result { case let .success(object): @@ -348,7 +347,7 @@ public extension BloodPressureService { } } - func replaceHeartRate(uuid: UUID, value: Int, date: Date) async -> Result { + func replaceHeartRate(uuid: UUID, value: Int, date: Date) async -> Result { let result = await fetchHeartRateObjectById(uuid: uuid) switch result { case let .success(object): @@ -357,10 +356,10 @@ public extension BloodPressureService { case .success: return await saveHeartRate(value: value, date: date) case .failure: - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } case .failure: - return .failure(AppError.healthKit(type: .updateItem)) + return .failure(HealthError.updateFailed) } } } diff --git a/Sources/OversizeHealthService/BodyMassService.swift b/Sources/OversizeHealthService/BodyMassService.swift index e32b44c..8c4165c 100644 --- a/Sources/OversizeHealthService/BodyMassService.swift +++ b/Sources/OversizeHealthService/BodyMassService.swift @@ -8,12 +8,11 @@ import Foundation import HealthKit #endif import OversizeCore -import OversizeModels #if os(iOS) || os(macOS) @available(iOS 15, macOS 13.0, *) public protocol BodyMassServiceProtocol { - func requestAuthorization() async -> Result + func requestAuthorization() async -> Result func fetchBodyMass() async throws -> HKStatisticsCollection? func calculateSteps(completion: @Sendable @escaping (HKStatisticsCollection?) -> Void) func getWeightData(forDay days: Int, completion: @Sendable @escaping (_ weight: Double?, _ date: Date?) -> Void) @@ -40,14 +39,15 @@ open class BodyMassService: @unchecked Sendable { @available(iOS 15, macOS 13.0, *) extension BodyMassService: BodyMassServiceProtocol { - public func requestAuthorization() async -> Result { - guard let healthStore, let type = bodyMassType else { return .failure(AppError.custom(title: "Not authorization")) } + public func requestAuthorization() async -> Result { + guard let healthStore else { return .failure(HealthError.accessDenied) } + guard let type = bodyMassType else { return .failure(HealthError.dataTypeNotAvailable) } do { try await healthStore.requestAuthorization(toShare: [type], read: [type]) return .success(true) } catch { - return .failure(AppError.custom(title: "Not get")) + return .failure(HealthError.unknown(error)) } } diff --git a/Sources/OversizeHealthService/HealthKitService.swift b/Sources/OversizeHealthService/HealthKitService.swift index acc314f..7f0166d 100644 --- a/Sources/OversizeHealthService/HealthKitService.swift +++ b/Sources/OversizeHealthService/HealthKitService.swift @@ -8,7 +8,6 @@ import Foundation import HealthKit #endif import OversizeCore -import OversizeModels #if os(iOS) || os(macOS) @available(iOS 15, macOS 13.0, *) @@ -24,40 +23,40 @@ open class HealthKitService: @unchecked Sendable { @available(iOS 15, macOS 13.0, *) extension HealthKitService { - func saveQuantitySample(_ quantitySample: HKQuantitySample) async -> Result { + func saveQuantitySample(_ quantitySample: HKQuantitySample) async -> Result { guard let healthStore else { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } do { try await healthStore.save(quantitySample) return .success(quantitySample) } catch { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } } - func saveCorrelation(_ correlation: HKCorrelation) async -> Result { + func saveCorrelation(_ correlation: HKCorrelation) async -> Result { guard let healthStore else { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } do { try await healthStore.save(correlation) return .success(correlation) } catch { - return .failure(AppError.healthKit(type: .savingItem)) + return .failure(HealthError.saveFailed) } } - func saveObjects(_ objects: [HKObject]) async -> Result<[HKObject], AppError> { + func saveObjects(_ objects: [HKObject]) async -> Result<[HKObject], Error> { do { let _ = try await healthStore?.save(objects) return .success(objects) } catch { - return .failure(.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } } - func fetchObjectById(uuid: UUID, type: HKQuantityType) async -> Result { + func fetchObjectById(uuid: UUID, type: HKQuantityType) async -> Result { await withCheckedContinuation { continuation in let predicate = HKQuery.predicateForObject(with: uuid) let query = HKSampleQuery( @@ -67,18 +66,18 @@ extension HealthKitService { sortDescriptors: nil, ) { _, results, error in if error != nil { - continuation.resume(returning: .failure(AppError.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } else if let item = results?.first { continuation.resume(returning: .success(item)) } else { - continuation.resume(returning: .failure(AppError.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } } healthStore?.execute(query) } } - func fetchHKQuantitySample(startDate: Date, endDate: Date = Date(), type: HKQuantityType) async -> Result<[HKQuantitySample], AppError> { + func fetchHKQuantitySample(startDate: Date, endDate: Date = Date(), type: HKQuantityType) async -> Result<[HKQuantitySample], Error> { await withCheckedContinuation { continuation in let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: HKQueryOptions.strictEndDate) @@ -91,14 +90,14 @@ extension HealthKitService { if let samples = results as? [HKQuantitySample] { continuation.resume(returning: .success(samples)) } else { - continuation.resume(returning: .failure(.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } } healthStore?.execute(query) } } - func fetchCorrelation(startDate: Date, endDate: Date = Date(), type: HKCorrelationType) async -> Result<[HKCorrelation], AppError> { + func fetchCorrelation(startDate: Date, endDate: Date = Date(), type: HKCorrelationType) async -> Result<[HKCorrelation], Error> { let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: HKQueryOptions.strictEndDate) return await withCheckedContinuation { continuation in let query = HKSampleQuery( @@ -110,14 +109,14 @@ extension HealthKitService { if let samples = results as? [HKCorrelation] { continuation.resume(returning: .success(samples)) } else { - continuation.resume(returning: .failure(AppError.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } } healthStore?.execute(query) } } - func fetchCorrelationById(uuid: UUID, type: HKCorrelationType) async -> Result { + func fetchCorrelationById(uuid: UUID, type: HKCorrelationType) async -> Result { await withCheckedContinuation { continuation in let predicate = HKQuery.predicateForObject(with: uuid) let query = HKSampleQuery( @@ -127,18 +126,18 @@ extension HealthKitService { sortDescriptors: nil, ) { _, results, error in if error != nil { - continuation.resume(returning: .failure(AppError.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } else if let item = results?.first { continuation.resume(returning: .success(item)) } else { - continuation.resume(returning: .failure(AppError.healthKit(type: .fetchItems))) + continuation.resume(returning: .failure(HealthError.fetchFailed)) } } healthStore?.execute(query) } } - func delete(type: HKObjectType, syncId: UUID) async -> Result { + func delete(type: HKObjectType, syncId: UUID) async -> Result { let predicate = HKQuery.predicateForObjects( withMetadataKey: HKMetadataKeySyncIdentifier, allowedValues: [syncId.uuidString], @@ -147,31 +146,31 @@ extension HealthKitService { let _ = try await healthStore?.deleteObjects(of: type, predicate: predicate) return .success(true) } catch { - return .failure(.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } } - func deleteObject(_ object: HKObject) async -> Result { + func deleteObject(_ object: HKObject) async -> Result { guard let healthStore else { - return .failure(AppError.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } do { try await healthStore.delete(object) return .success(object) } catch { - return .failure(AppError.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } } - func deleteObjects(_ objects: [HKObject]) async -> Result<[HKObject], AppError> { + func deleteObjects(_ objects: [HKObject]) async -> Result<[HKObject], Error> { guard let healthStore else { - return .failure(AppError.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } do { try await healthStore.delete(objects) return .success(objects) } catch { - return .failure(AppError.healthKit(type: .deleteItem)) + return .failure(HealthError.deleteFailed) } } } diff --git a/Sources/OversizeLocationService/LocationService.swift b/Sources/OversizeLocationService/LocationService.swift index 060aacb..9122683 100644 --- a/Sources/OversizeLocationService/LocationService.swift +++ b/Sources/OversizeLocationService/LocationService.swift @@ -6,12 +6,12 @@ import CoreLocation import Foundation import MapKit -import OversizeModels +import OversizeCore public protocol LocationServiceProtocol: Sendable { func currentLocation() async throws -> CLLocationCoordinate2D? func systemPermissionsStatus() -> CLAuthorizationStatus - func permissionsStatus() -> Result + func permissionsStatus() -> Result func fetchCoordinateFromAddress(_ address: String) async throws -> CLLocationCoordinate2D func fetchAddressFromLocation(_ location: CLLocationCoordinate2D) async throws -> LocationAddress } @@ -47,17 +47,17 @@ extension LocationService: LocationServiceProtocol { return locationManager.authorizationStatus } - public func permissionsStatus() -> Result { + public func permissionsStatus() -> Result { locationManager.requestWhenInUseAuthorization() switch locationManager.authorizationStatus { case .notDetermined: - return .failure(.location(type: .notDetermined)) + return .failure(LocationError.notDetermined) case .denied: - return .failure(.location(type: .notAccess)) + return .failure(LocationError.notAccess) case .restricted, .authorizedAlways, .authorizedWhenInUse: return .success(true) @unknown default: - return .failure(.location(type: .unknown)) + return .failure(LocationError.unknown(nil)) } } diff --git a/Sources/OversizeNotificationService/LocalNotificationService.swift b/Sources/OversizeNotificationService/LocalNotificationService.swift index 3b66610..8b5c77f 100644 --- a/Sources/OversizeNotificationService/LocalNotificationService.swift +++ b/Sources/OversizeNotificationService/LocalNotificationService.swift @@ -4,7 +4,6 @@ // import OversizeCore -import OversizeModels import SwiftUI @preconcurrency import UserNotifications @@ -16,7 +15,7 @@ public protocol LocalNotificationServiceProtocol: Sendable { func scheduleNotification(id: UUID, title: String, body: String, timeInterval: Double, repeatNotification: Bool, scheduleType: LocalNotification.ScheduleType, dateComponents: DateComponents) async func fetchPendingIds() async -> [String] func removeRequest(withIdentifier identifier: String) - func requestAccess() async -> Result + func requestAccess() async -> Result } public final class LocalNotificationService: NSObject, @unchecked Sendable { @@ -43,22 +42,22 @@ extension LocalNotificationService: LocalNotificationServiceProtocol { isGranted = (currentSettings.authorizationStatus == .authorized) } - public func requestAccess() async -> Result { + public func requestAccess() async -> Result { let _ = try? await requestAuthorization() let currentSettings = await notificationCenter.notificationSettings() switch currentSettings.authorizationStatus { case .notDetermined: - return .failure(.notifications(type: .notDetermined)) + return .failure(NotificationError.permissionNotDetermined) case .denied: - return .failure(.notifications(type: .notAccess)) + return .failure(NotificationError.accessDenied) case .authorized: return .success(true) case .provisional: - return .failure(.notifications(type: .notAccess)) + return .failure(NotificationError.accessDenied) case .ephemeral: - return .failure(.notifications(type: .notAccess)) + return .failure(NotificationError.accessDenied) @unknown default: - return .failure(.notifications(type: .unknown)) + return .failure(NotificationError.unknown(nil)) } } diff --git a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift index 1352da5..81ba6b7 100644 --- a/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift +++ b/Sources/OversizeServices/FeatureFlags/FeatureFlags.swift @@ -6,7 +6,6 @@ import Foundation public enum FeatureFlags: Sendable { - private static var featureFlagsDict: [String: Any]? { Bundle.main.infoDictionary?["FeatureFlags"] as? [String: Any] } @@ -63,7 +62,7 @@ public enum FeatureFlags: Sendable { public static var lookscreen: Bool? { getBool("Lookscreen") } - + public static var blurMinimize: Bool? { getBool("BlurMinimize") } diff --git a/Sources/OversizeServices/Info/Deprecated.swift b/Sources/OversizeServices/Info/Deprecated.swift index 4729e31..c76caf9 100644 --- a/Sources/OversizeServices/Info/Deprecated.swift +++ b/Sources/OversizeServices/Info/Deprecated.swift @@ -23,98 +23,158 @@ public extension Info { public extension Info.App { @available(*, deprecated, renamed: "appStoreId") - static var appStoreID: String? { appStoreId } + static var appStoreID: String? { + appStoreId + } @available(*, deprecated, renamed: "bundleId") - static var bundleID: String? { bundleId } + static var bundleID: String? { + bundleId + } @available(*, deprecated, renamed: "telegramChatId") - static var telegramChatID: String? { telegramChatId } + static var telegramChatID: String? { + telegramChatId + } @available(*, deprecated, renamed: "alternateIconNames") - static var alternateIconsNames: [String] { alternateIconNames } + static var alternateIconsNames: [String] { + alternateIconNames + } @available(*, deprecated, renamed: "osVersion") - @MainActor static var system: String? { osVersion } + @MainActor static var system: String? { + osVersion + } @available(*, deprecated, renamed: "localeIdentifier") - static var language: String? { localeIdentifier } + static var language: String? { + localeIdentifier + } @available(*, deprecated, renamed: "appStoreUrl") - static var appStoreURL: URL? { appStoreUrl } + static var appStoreURL: URL? { + appStoreUrl + } @available(*, deprecated, renamed: "appStoreReviewUrl") - static var appStoreReviewURL: URL? { appStoreReviewUrl } + static var appStoreReviewURL: URL? { + appStoreReviewUrl + } @available(*, deprecated, renamed: "websiteUrl") - static var websiteURL: URL? { websiteUrl } + static var websiteURL: URL? { + websiteUrl + } @available(*, deprecated, renamed: "privacyPolicyUrl") - static var privacyPolicyURL: URL? { privacyPolicyUrl } + static var privacyPolicyURL: URL? { + privacyPolicyUrl + } @available(*, deprecated, renamed: "termsOfUseUrl") - static var termsOfUseURL: URL? { termsOfUseUrl } + static var termsOfUseURL: URL? { + termsOfUseUrl + } @available(*, deprecated, renamed: "telegramChatUrl") - static var telegramChatURL: URL? { telegramChatUrl } + static var telegramChatURL: URL? { + telegramChatUrl + } } public extension Info.Developer { @available(*, deprecated, renamed: "websiteUrl") - static var url: String? { websiteUrl?.absoluteString } + static var url: String? { + websiteUrl?.absoluteString + } @available(*, deprecated, renamed: "websiteUrl") - static var website: String? { websiteUrl?.absoluteString } + static var website: String? { + websiteUrl?.absoluteString + } @available(*, deprecated, renamed: "emailUrl") - static var emailURL: URL? { emailUrl } + static var emailURL: URL? { + emailUrl + } @available(*, deprecated, renamed: "appsUrl") - static var appsURL: URL? { appsUrl } + static var appsURL: URL? { + appsUrl + } } public extension Info.Company { @available(*, deprecated, renamed: "websiteUrl") - static var url: URL? { websiteUrl } + static var url: URL? { + websiteUrl + } @available(*, deprecated, renamed: "appStoreId") - static var appStoreID: String? { appStoreId } + static var appStoreID: String? { + appStoreId + } @available(*, deprecated, renamed: "twitterUsername") - static var twitterID: String? { twitterUsername } + static var twitterID: String? { + twitterUsername + } @available(*, deprecated, renamed: "dribbbleUsername") - static var dribbbleID: String? { dribbbleUsername } + static var dribbbleID: String? { + dribbbleUsername + } @available(*, deprecated, renamed: "instagramUsername") - static var instagramID: String? { instagramUsername } + static var instagramID: String? { + instagramUsername + } @available(*, deprecated, renamed: "facebookUsername") - static var facebookID: String? { facebookUsername } + static var facebookID: String? { + facebookUsername + } @available(*, deprecated, renamed: "telegramUsername") - static var telegramID: String? { telegramUsername } + static var telegramID: String? { + telegramUsername + } @available(*, deprecated, renamed: "cdnUrl") - static var cdnURL: URL? { cdnUrl } + static var cdnURL: URL? { + cdnUrl + } @available(*, deprecated, renamed: "emailUrl") - static var emailURL: URL? { emailUrl } + static var emailURL: URL? { + emailUrl + } @available(*, deprecated, renamed: "telegramUrl") - static var telegramURL: URL? { telegramUrl } + static var telegramURL: URL? { + telegramUrl + } @available(*, deprecated, renamed: "facebookUrl") - static var facebookURL: URL? { facebookUrl } + static var facebookURL: URL? { + facebookUrl + } @available(*, deprecated, renamed: "twitterUrl") - static var twitterURL: URL? { twitterUrl } + static var twitterURL: URL? { + twitterUrl + } @available(*, deprecated, renamed: "dribbbleUrl") - static var dribbbleURL: URL? { dribbbleUrl } + static var dribbbleURL: URL? { + dribbbleUrl + } @available(*, deprecated, renamed: "instagramUrl") - static var instagramURL: URL? { instagramUrl } + static var instagramURL: URL? { + instagramUrl + } } // MARK: - Deprecated URLs Enum @@ -123,73 +183,119 @@ public extension Info.Company { public extension Info { enum URLs: Sendable { @available(*, deprecated, renamed: "Info.App.appStoreReviewUrl") - public static var appStoreReview: URL? { App.appStoreReviewUrl } + public static var appStoreReview: URL? { + App.appStoreReviewUrl + } @available(*, deprecated, renamed: "Info.App.appStoreUrl") - public static var appStore: URL? { App.appStoreUrl } + public static var appStore: URL? { + App.appStoreUrl + } @available(*, deprecated, renamed: "Info.Developer.emailUrl") - public static var developerEmail: URL? { Developer.emailUrl } + public static var developerEmail: URL? { + Developer.emailUrl + } @available(*, deprecated, renamed: "Info.Developer.appsUrl") - public static var developerApps: URL? { Developer.appsUrl } + public static var developerApps: URL? { + Developer.appsUrl + } @available(*, deprecated, renamed: "Info.App.telegramChatUrl") - public static var telegramChat: URL? { App.telegramChatUrl } + public static var telegramChat: URL? { + App.telegramChatUrl + } @available(*, deprecated, renamed: "Info.App.websiteUrl") - public static var app: URL? { App.websiteUrl } + public static var app: URL? { + App.websiteUrl + } @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") - public static var privacyPolicy: URL? { App.privacyPolicyUrl } + public static var privacyPolicy: URL? { + App.privacyPolicyUrl + } @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") - public static var termsOfUse: URL? { App.termsOfUseUrl } + public static var termsOfUse: URL? { + App.termsOfUseUrl + } @available(*, deprecated, renamed: "Info.Company.cdnUrl") - public static var companyCDN: URL? { Company.cdnUrl } + public static var companyCDN: URL? { + Company.cdnUrl + } @available(*, deprecated, renamed: "Info.Company.emailUrl") - public static var companyEmail: URL? { Company.emailUrl } + public static var companyEmail: URL? { + Company.emailUrl + } @available(*, deprecated, renamed: "Info.Company.telegramUrl") - public static var companyTelegram: URL? { Company.telegramUrl } + public static var companyTelegram: URL? { + Company.telegramUrl + } @available(*, deprecated, renamed: "Info.Company.facebookUrl") - public static var companyFacebook: URL? { Company.facebookUrl } + public static var companyFacebook: URL? { + Company.facebookUrl + } @available(*, deprecated, renamed: "Info.Company.twitterUrl") - public static var companyTwitter: URL? { Company.twitterUrl } + public static var companyTwitter: URL? { + Company.twitterUrl + } @available(*, deprecated, renamed: "Info.Company.dribbbleUrl") - public static var companyDribbble: URL? { Company.dribbbleUrl } + public static var companyDribbble: URL? { + Company.dribbbleUrl + } @available(*, deprecated, renamed: "Info.Company.instagramUrl") - public static var companyInstagram: URL? { Company.instagramUrl } + public static var companyInstagram: URL? { + Company.instagramUrl + } @available(*, deprecated, renamed: "Info.App.appStoreUrl") - public static var appInstallShare: URL? { App.appStoreUrl } + public static var appInstallShare: URL? { + App.appStoreUrl + } @available(*, deprecated, renamed: "Info.Developer.emailUrl") - public static var developerSendMail: URL? { Developer.emailUrl } + public static var developerSendMail: URL? { + Developer.emailUrl + } @available(*, deprecated, renamed: "Info.Developer.appsUrl") - public static var developerAllApps: URL? { Developer.appsUrl } + public static var developerAllApps: URL? { + Developer.appsUrl + } @available(*, deprecated, renamed: "Info.App.telegramChatUrl") - public static var appTelegramChat: URL? { App.telegramChatUrl } + public static var appTelegramChat: URL? { + App.telegramChatUrl + } @available(*, deprecated, renamed: "Info.App.websiteUrl") - public static var appUrl: URL? { App.websiteUrl } + public static var appUrl: URL? { + App.websiteUrl + } @available(*, deprecated, renamed: "Info.App.privacyPolicyUrl") - public static var appPrivacyPolicyUrl: URL? { App.privacyPolicyUrl } + public static var appPrivacyPolicyUrl: URL? { + App.privacyPolicyUrl + } @available(*, deprecated, renamed: "Info.App.termsOfUseUrl") - public static var appTermsOfUseUrl: URL? { App.termsOfUseUrl } + public static var appTermsOfUseUrl: URL? { + App.termsOfUseUrl + } @available(*, deprecated, renamed: "Info.Company.cdnUrl") - public static var companyCdnUrl: URL? { Company.cdnUrl } + public static var companyCdnUrl: URL? { + Company.cdnUrl + } } } @@ -285,8 +391,7 @@ public final class PlistService: Sendable { guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { return [] } - let value: [String] = links[field] as? [String] ?? [] - return value + return links[field] as? [String] ?? [] } public func getBoolFromDictionary(field: String, dictionary: String, plist: String) -> Bool? { @@ -297,8 +402,7 @@ public final class PlistService: Sendable { guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { return nil } - let value: Bool? = links[field] as? Bool - return value + return links[field] as? Bool } public func getIntFromDictionary(field: String, dictionary: String, plist: String) -> Int? { @@ -309,8 +413,7 @@ public final class PlistService: Sendable { guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { return nil } - let value: Int? = links[field] as? Int - return value + return links[field] as? Int } public func getStringFromDictionary(field: String, dictionary: String, plist: String) -> String? { @@ -321,8 +424,7 @@ public final class PlistService: Sendable { guard let links = plist?.object(forKey: dictionary) as? [String: Any] else { return nil } - let value: String? = links[field] as? String - return value + return links[field] as? String } public func getString(field: String, plist: String) -> String? { diff --git a/Sources/OversizeServices/Services/SettingsService/SettingsService.swift b/Sources/OversizeServices/Services/SettingsService/SettingsService.swift index 903fc3e..27a00da 100644 --- a/Sources/OversizeServices/Services/SettingsService/SettingsService.swift +++ b/Sources/OversizeServices/Services/SettingsService/SettingsService.swift @@ -70,7 +70,7 @@ public final class SettingsService: ObservableObject, SettingsServiceProtocol, @ @SecureStorage(Keys.pinCode) private var pinCode } -// PIN Code +/// PIN Code public extension SettingsService { func getPINCode() -> String { logSecurity("Get PIN Code") diff --git a/Sources/OversizeStoreService/Models/StoreSpecialOfferEventType.swift b/Sources/OversizeStoreService/Models/StoreSpecialOfferEventType.swift index 10e978d..c4a872a 100644 --- a/Sources/OversizeStoreService/Models/StoreSpecialOfferEventType.swift +++ b/Sources/OversizeStoreService/Models/StoreSpecialOfferEventType.swift @@ -40,8 +40,7 @@ public enum StoreSpecialOfferEventType: String, Identifiable, CaseIterable, Hash public var eventInterval: DateInterval? { let caledar = Calendar.current guard let startDate = eventStartDate else { return nil } - let interval = caledar.dateInterval(of: .day, for: startDate) - return interval + return caledar.dateInterval(of: .day, for: startDate) } public var isNow: Bool { diff --git a/Sources/OversizeStoreService/StoreKitService.swift b/Sources/OversizeStoreService/StoreKitService.swift index de26a8b..a14916f 100644 --- a/Sources/OversizeStoreService/StoreKitService.swift +++ b/Sources/OversizeStoreService/StoreKitService.swift @@ -5,7 +5,6 @@ import Foundation import OversizeCore -import OversizeModels import OversizeServices import StoreKit @@ -40,7 +39,7 @@ public enum SubscriptionTier: Int, Comparable, Sendable { } public final class StoreKitService: Sendable { - public func requestProducts(productIds: [String]) async -> Result { + public func requestProducts(productIds: [String]) async -> Result { do { let storeProducts = try await Product.products(for: productIds) @@ -74,11 +73,11 @@ public final class StoreKitService: Sendable { return .success(products) } catch { logError("Failed product request from the App Store server", error: error) - return .failure(.custom(title: "Failed product request from the App Store server")) + return .failure(CustomError(title: "Failed product request from the App Store server")) } } - public func purchase(_ product: Product) async throws -> Result { + public func purchase(_ product: Product) async throws -> Result { let result = try await product.purchase() switch result { @@ -88,9 +87,9 @@ public final class StoreKitService: Sendable { return .success(transaction) case .userCancelled, .pending: - return .failure(.custom(title: "Error")) + return .failure(CustomError(title: "Error")) default: - return .failure(.custom(title: "Error")) + return .failure(CustomError(title: "Error")) } } @@ -118,7 +117,7 @@ public final class StoreKitService: Sendable { } @MainActor - public func updateCustomerProductStatus(products: StoreKitProducts) async -> Result { + public func updateCustomerProductStatus(products: StoreKitProducts) async -> Result { var purchasedNonConsumable: [Product] = [] var purchasedAutoRenewable: [Product] = [] var purchasedNonRenewable: [Product] = [] @@ -154,7 +153,7 @@ public final class StoreKitService: Sendable { } } catch { - return .failure(.custom(title: error.localizedDescription)) + return .failure(CustomError(title: error.localizedDescription)) } } @@ -213,7 +212,7 @@ public final class StoreKitService: Sendable { products.sorted(by: { $0.price < $1.price }) } - // Get a subscription's level of service using the product ID. + /// Get a subscription's level of service using the product ID. public func tier(for productId: String) -> SubscriptionTier { if productId.contains(".yearly") { .yearly @@ -261,7 +260,7 @@ public final class StoreKitService: Sendable { } public func paymentTypeLabel(paymentMode: Product.SubscriptionOffer.PaymentMode) -> String { - let trialTypeLabel: String = if #available(iOS 15.4, macOS 12.3, tvOS 15.4, *) { + if #available(iOS 15.4, macOS 12.3, tvOS 15.4, *) { paymentMode.localizedDescription } else { switch paymentMode { @@ -275,7 +274,6 @@ public final class StoreKitService: Sendable { "" } } - return trialTypeLabel } public func salePercent(product: Product, products: StoreKitProducts) -> Decimal { diff --git a/Tests/OversizeServicesTests/OversizeServicesTests.swift b/Tests/OversizeServicesTests/OversizeServicesTests.swift index 5cc94d4..bf06c8d 100644 --- a/Tests/OversizeServicesTests/OversizeServicesTests.swift +++ b/Tests/OversizeServicesTests/OversizeServicesTests.swift @@ -12,7 +12,7 @@ final class OversizeServicesTests: XCTestCase { Container.shared.appStateService.reset() } - func testAppRunCount() throws { + func testAppRunCount() { Container.shared.appStateService.register { AppStateService() } let newRunCount = Container.shared.appStateService().appRunCount + 1 Container.shared.appStateService().appRun() From 846fb34d5063a5fb31170f191f9cef9698e431e1 Mon Sep 17 00:00:00 2001 From: Alexander Romanov Date: Mon, 23 Feb 2026 15:00:28 +0300 Subject: [PATCH 5/5] Fix CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdd76bb..f0d6b8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,9 @@ jobs: - OversizeNotificationService - OversizeFileManagerService destination: - - platform=iOS Simulator,name=iPhone 16,OS=18.1 - - platform=watchOS Simulator,name=Apple Watch SE (40mm) (2nd generation),OS=11.1 - - platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=18.1 + - platform=iOS Simulator,name=iPhone 17,OS=26.2 + - platform=watchOS Simulator,name=Apple Watch SE 3 (40mm),OS=26.2 + - platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=26.2 - platform=macOS,arch=arm64 with: