├── Sources └── Sonar │ ├── APIs │ ├── Result.swift │ ├── BugTracker.swift │ ├── SonarError.swift │ ├── OpenRadarRouter.swift │ ├── OpenRadar.swift │ ├── AppleRadarRouter.swift │ └── AppleRadar.swift │ ├── Models │ ├── Classification.swift │ ├── Reproducibility.swift │ ├── Area.swift │ ├── Product.swift │ ├── Attachment.swift │ ├── Radar.swift │ └── ModelsData.swift │ ├── Extensions │ └── String+Regex.swift │ └── Sonar.swift ├── Makefile ├── .travis.yml ├── Package.pins ├── Package.resolved ├── Package.swift ├── Sonar.podspec ├── LICENSE ├── .gitignore └── README.md /Sources/Sonar/APIs/Result.swift: -------------------------------------------------------------------------------- 1 | public enum Result { 2 | case success(Value) 3 | case failure(Error) 4 | } 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | package: 2 | swift build -Xswiftc -warnings-as-errors 3 | 4 | ci: 5 | ifndef ACTION 6 | $(error ACTION is not defined) 7 | endif 8 | make $(ACTION) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | env: ACTION=package 5 | 6 | language: objective-c 7 | osx_image: xcode9.3 8 | 9 | install: true 10 | script: 11 | - make ci 12 | -------------------------------------------------------------------------------- /Package.pins: -------------------------------------------------------------------------------- 1 | { 2 | "autoPin": true, 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "reason": null, 7 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git", 8 | "version": "4.4.0" 9 | } 10 | ], 11 | "version": 1 12 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2fb881a1702cb1976c36192aceb54dcedab6fdc2", 10 | "version": "4.7.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Sonar", 6 | products: [ 7 | .library(name: "Sonar", targets: ["Sonar"]), 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMinor(from: "4.7.2")) 11 | ], 12 | targets: [ 13 | .target(name: "Sonar", dependencies: ["Alamofire"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Classification.swift: -------------------------------------------------------------------------------- 1 | public struct Classification: Equatable { 2 | /// Internal apple's identifier 3 | public let appleIdentifier: Int 4 | 5 | /// The name of the classification; useful to use as display name (e.g. 'UI/Usability'). 6 | public let name: String 7 | 8 | public static func == (lhs: Classification, rhs: Classification) -> Bool { 9 | return lhs.appleIdentifier == rhs.appleIdentifier && lhs.name == rhs.name 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Reproducibility.swift: -------------------------------------------------------------------------------- 1 | public struct Reproducibility: Equatable { 2 | /// Internal apple's identifier 3 | public let appleIdentifier: Int 4 | 5 | /// The name of the reproducibility; useful to use as display name (e.g. 'Sometimes'). 6 | public let name: String 7 | 8 | public static func == (lhs: Reproducibility, rhs: Reproducibility) -> Bool { 9 | return lhs.appleIdentifier == rhs.appleIdentifier && lhs.name == rhs.name 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Area.swift: -------------------------------------------------------------------------------- 1 | public struct Area: Equatable { 2 | /// Internal apple's identifier 3 | public let appleIdentifier: Int 4 | 5 | /// The name of the area; useful to use as display name (e.g. 'App Switcher'). 6 | public let name: String 7 | 8 | public init(appleIdentifier: Int, name: String) { 9 | self.appleIdentifier = appleIdentifier 10 | self.name = name 11 | } 12 | 13 | public static func == (lhs: Area, rhs: Area) -> Bool { 14 | return lhs.appleIdentifier == rhs.appleIdentifier && lhs.name == rhs.name 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Product.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Product: Equatable { 4 | /// Internal apple's identifier 5 | public let appleIdentifier: Int 6 | /// The category of the product (e.g. 'Hardware'). 7 | public let category: String 8 | /// The name of the product; useful to use as display name (e.g. 'iOS'). 9 | public let name: String 10 | 11 | public static func == (lhs: Product, rhs: Product) -> Bool { 12 | return lhs.appleIdentifier == rhs.appleIdentifier 13 | && lhs.category == rhs.category 14 | && lhs.name == rhs.name 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sonar.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Sonar" 3 | s.version = "0.0.1" 4 | s.summary = "Apple's radar communication in Swift" 5 | s.homepage = "https://github.com/br1sk/Sonar" 6 | s.license = "MIT" 7 | s.author = { "Martin Conte Mac Donell" => "reflejo@gmail.com" } 8 | s.ios.deployment_target = "9.0" 9 | s.osx.deployment_target = "10.11" 10 | s.source = { :git => "https://github.com/br1sk/Sonar.git" } 11 | s.source_files = "Sources/Sonar/**/*.swift" 12 | s.requires_arc = true 13 | s.dependency "Alamofire", "~> 4.7.2" 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Keith Smiley (http://keith.so) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the 'Software'), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | .DS_Store 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | Packages/ 37 | .build/ 38 | 39 | # fastlane 40 | # 41 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 42 | # screenshots whenever they are needed. 43 | # For more information about the recommended setup visit: 44 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 45 | 46 | fastlane/report.xml 47 | fastlane/Preview.html 48 | fastlane/screenshots 49 | fastlane/test_output 50 | 51 | Example/run 52 | -------------------------------------------------------------------------------- /Sources/Sonar/Extensions/String+Regex.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// Returns the string matching in the given group number (if any), nil otherwise. 5 | /// 6 | /// - parameter pattern: The regular expression to match. 7 | /// - parameter group: The group number to return from the match. 8 | /// - parameter options: Options that will be use for matching using regular expressions. 9 | /// 10 | /// - returns: The string from the given group on the match (if any). 11 | func match(pattern: String, group: Int, options: NSRegularExpression.Options = []) -> String? { 12 | do { 13 | let regex = try NSRegularExpression(pattern: pattern, options: options) 14 | let range = NSRange(location: 0, length: self.count) 15 | 16 | guard let match = regex.firstMatch(in: self, options: [], range: range) else { 17 | return nil 18 | } 19 | 20 | if match.numberOfRanges < group { 21 | return nil 22 | } 23 | 24 | let matchRange = match.range(at: group) 25 | let startRange = self.index(self.startIndex, offsetBy: matchRange.location) 26 | let endRange = self.index(startRange, offsetBy: matchRange.length) 27 | 28 | return self[startRange ..< endRange].trimmingCharacters(in: .whitespacesAndNewlines) 29 | } catch { 30 | return nil 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/BugTracker.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | 3 | /// The service authentication method. 4 | /// 5 | /// - appleRadar: Apple's radar system. Authenticated by appleID / password. 6 | /// - openRadar: Open radar system. Authenticated by a non expiring token. 7 | public enum ServiceAuthentication { 8 | case appleRadar(appleID: String, password: String) 9 | case openRadar(token: String) 10 | } 11 | 12 | /// This protocol represents a bug tracker (such as Apple's radar or Open Radar). 13 | protocol BugTracker { 14 | /// Login into bug tracker. This method will use the authentication information provided by the service 15 | /// enum. 16 | /// 17 | /// - parameter getTwoFactorCode: A closure to retrieve a two factor auth code from the user. 18 | /// - parameter closure: A closure that will be called when the login is completed, 19 | /// on failure a `SonarError`. 20 | func login(getTwoFactorCode: @escaping (_ closure: @escaping (_ code: String?) -> Void) -> Void, 21 | closure: @escaping (Result) -> Void) 22 | 23 | /// Creates a new ticket into the bug tracker (needs authentication first). 24 | /// 25 | /// - parameter radar: The radar model with the information for the ticket. 26 | /// - parameter closure: A closure that will be called when the login is completed, on success it will 27 | /// contain a radar ID; on failure a `SonarError`. 28 | func create(radar: Radar, closure: @escaping (Result) -> Void) 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/SonarError.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | /// Represents an error on the communication and/or response parsing. 5 | public struct SonarError: Error { 6 | /// The message that represents the error condition. 7 | public let message: String 8 | 9 | /// Used to catch the things we can't gracefully fail. 10 | static let unknownError = SonarError(message: "Unknown error") 11 | 12 | /// Used when the token expires. 13 | static let authenticationError = SonarError(message: "Unauthorized, perhaps you have the wrong password") 14 | 15 | /// Use when a 412 is received. This happens when you haven't accepted a new agreement 16 | static let preconditionError = SonarError( 17 | message: "Precondition failed, try logging in on the web to validate your Apple developer account") 18 | 19 | init(message: String) { 20 | self.message = message 21 | } 22 | 23 | /// Factory to create a `SonarError` based on a `Response`. 24 | /// 25 | /// - parameter response: The HTTP resposne that is known to be failed. 26 | /// 27 | /// - returns: The error representing the problem. 28 | static func from(_ response: DataResponse) -> SonarError { 29 | switch response.response?.statusCode { 30 | case 401: 31 | return .authenticationError 32 | case 412: 33 | return .preconditionError 34 | default: 35 | break 36 | } 37 | 38 | switch response.result { 39 | case .failure(let error as NSError): 40 | return SonarError(message: error.localizedDescription) 41 | default: 42 | return .unknownError 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sonar 2 | 3 | An interface to create radars on [Apple's bug tracker](https://radar.apple.com) 4 | and [Open Radar](https://openradar.appspot.com/) frictionless from swift. 5 | 6 | ## Example 7 | 8 | ### Login 9 | 10 | ```swift 11 | let openRadar = Sonar(service: .openRadar(token: "abcdefg")) 12 | openRadar.login( 13 | getTwoFactorCode: { _ in fatalError("OpenRadar doesn't support 2 factor" }) 14 | { result in 15 | guard case let .success = result else { 16 | return 17 | } 18 | 19 | print("Logged in!") 20 | } 21 | ``` 22 | 23 | ### Create radar 24 | 25 | ```swift 26 | let radar = Radar( 27 | classification: .feature, product: .bugReporter, reproducibility: .always, 28 | title: "Add REST API to Radar", description: "Add REST API to Radar", steps: "N/A", 29 | expected: "Radar to have a REST API available", actual: "API not provided", 30 | configuration: "N/A", version: "Any", notes: "N/A", attachments: [] 31 | ) 32 | 33 | let openRadar = Sonar(service: .openRadar(token: "abcdefg")) 34 | openRadar.create(radar: radar) { result in 35 | // Check to see if the request succeeded 36 | } 37 | ``` 38 | 39 | ### Login and Create radar on the same call 40 | 41 | ```swift 42 | let radar = Radar( 43 | classification: .feature, product: .bugReporter, reproducibility: .always, 44 | title: "Add REST API to Radar", description: "Add REST API to Radar", steps: "N/A", 45 | expected: "Radar to have a REST API available", actual: "API not provided", 46 | configuration: "N/A", version: "Any", notes: "N/A", attachments: [] 47 | ) 48 | 49 | let appleRadar = Sonar(service: .appleRadar(appleID: "a", password: "b")) 50 | appleRadar.loginThenCreate( 51 | radar: radar, 52 | getTwoFactorCode: { closure in 53 | let code = // Somehow get 2 factor auth code for user 54 | closure(code) 55 | }) 56 | { result in 57 | switch result { 58 | case .success(let value): 59 | print(value) // This is the radar ID! 60 | case .failure(let error): 61 | print(error) 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Attachment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(OSX) 3 | import CoreServices 4 | #else 5 | import MobileCoreServices 6 | #endif 7 | 8 | public enum AttachmentError: Error { 9 | // Error thrown with the MIME type for the attachment is not found 10 | case invalidMimeType(fileExtension: String) 11 | } 12 | 13 | public struct Attachment: Equatable { 14 | // The filename of the attachment, used for display on the web 15 | public let filename: String 16 | // The MIME type of the attachmenth 17 | public let mimeType: String 18 | // The data from the attachment, read at the time of attachment 19 | public let data: Data 20 | 21 | // The size of the attachment (based on the data) 22 | public var size: Int { 23 | return self.data.count 24 | } 25 | 26 | public init(url: URL) throws { 27 | self.init(filename: url.lastPathComponent, mimeType: try getMimeType(for: url.pathExtension), 28 | data: try Data(contentsOf: url)) 29 | } 30 | 31 | public init(filename: String, mimeType: String, data: Data) { 32 | self.filename = filename 33 | self.mimeType = mimeType 34 | self.data = data 35 | } 36 | } 37 | 38 | public func == (lhs: Attachment, rhs: Attachment) -> Bool { 39 | return lhs.filename == rhs.filename && lhs.size == rhs.size && lhs.data == rhs.data 40 | } 41 | 42 | private func getMimeType(for fileExtension: String) throws -> String { 43 | guard let identifier = UTTypeCreatePreferredIdentifierForTag( 44 | kUTTagClassFilenameExtension, fileExtension as CFString, nil)?.takeRetainedValue() else 45 | { 46 | throw AttachmentError.invalidMimeType(fileExtension: fileExtension) 47 | } 48 | 49 | if let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType) { 50 | return mimeType.takeRetainedValue() as String 51 | } else if UTTypeConformsTo(identifier, kUTTypePlainText) { 52 | return "text/plain" 53 | } 54 | 55 | throw AttachmentError.invalidMimeType(fileExtension: fileExtension) 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/OpenRadarRouter.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | /// Open radar request router. 5 | /// 6 | /// - create: The `Route` used to create a new radar. 7 | enum OpenRadarRouter { 8 | case create(radar: Radar) 9 | 10 | fileprivate static let baseURL = URL(string: "https://openradar.appspot.com")! 11 | 12 | /// The request components including headers and parameters. 13 | var components: (path: String, method: Alamofire.HTTPMethod, parameters: [String: String]) { 14 | switch self { 15 | case .create(let radar): 16 | let formatter = DateFormatter() 17 | formatter.dateFormat = "dd-MMM-yyyy hh:mm a" 18 | formatter.locale = Locale(identifier: "en_US_POSIX") 19 | 20 | return (path: "/api/radars/add", method: .post, parameters: [ 21 | "classification": radar.classification.name, 22 | "description": radar.body, 23 | "number": radar.ID.map { String($0) } ?? "", 24 | "originated": formatter.string(from: Date()), 25 | "product": radar.product.name, 26 | "product_version": radar.version, 27 | "reproducible": radar.reproducibility.name, 28 | "status": "Open", 29 | "title": radar.title, 30 | ]) 31 | } 32 | } 33 | } 34 | 35 | extension OpenRadarRouter: URLRequestConvertible { 36 | /// The URL that will be used for the request. 37 | var url: URL { 38 | return self.urlRequest!.url! 39 | } 40 | 41 | /// The request representation of the route including parameters and HTTP method. 42 | func asURLRequest() -> URLRequest { 43 | let (path, method, parameters) = self.components 44 | let url = OpenRadarRouter.baseURL.appendingPathComponent(path) 45 | 46 | var request = URLRequest(url: url) 47 | request.httpMethod = method.rawValue 48 | return try! URLEncoding().encode(request, with: parameters) 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/OpenRadar.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | class OpenRadar: BugTracker { 5 | private let manager: Alamofire.SessionManager 6 | 7 | init(token: String) { 8 | let configuration = URLSessionConfiguration.default 9 | configuration.httpAdditionalHeaders = ["Authorization": token] 10 | 11 | self.manager = Alamofire.SessionManager(configuration: configuration) 12 | } 13 | 14 | /// Login into open radar. This is actually a NOP for now (token is saved into the session). 15 | /// 16 | /// - parameter getTwoFactorCode: A closure to retrieve a two factor auth code from the user 17 | /// - parameter closure: A closure that will be called when the login is completed, on success it 18 | /// will contain a list of `Product`s; on failure a `SonarError`. 19 | func login(getTwoFactorCode: @escaping (_ closure: @escaping (_ code: String?) -> Void) -> Void, 20 | closure: @escaping (Result) -> Void) 21 | { 22 | closure(.success(())) 23 | } 24 | 25 | /// Creates a new ticket into open radar (needs authentication first). 26 | /// 27 | /// - parameter radar: The radar model with the information for the ticket. 28 | /// - parameter closure: A closure that will be called when the login is completed, on success it will 29 | /// contain a radar ID; on failure a `SonarError`. 30 | func create(radar: Radar, closure: @escaping (Result) -> Void) { 31 | guard let ID = radar.ID else { 32 | closure(.failure(SonarError(message: "Invalid radar ID"))) 33 | return 34 | } 35 | 36 | self.manager 37 | .request(OpenRadarRouter.create(radar: radar)) 38 | .validate() 39 | .responseJSON { response in 40 | guard case .success = response.result else { 41 | closure(.failure(SonarError.from(response))) 42 | return 43 | } 44 | 45 | closure(.success(ID)) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Sonar/Sonar.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | public class Sonar { 5 | private let tracker: BugTracker 6 | 7 | public init(service: ServiceAuthentication) { 8 | switch service { 9 | case .appleRadar(let appleID, let password): 10 | self.tracker = AppleRadar(appleID: appleID, password: password) 11 | 12 | case .openRadar(let token): 13 | self.tracker = OpenRadar(token: token) 14 | } 15 | } 16 | 17 | /// Login into bug tracker. This method will use the authentication information provided by the service enum. 18 | /// 19 | /// - parameter getTwoFactorCode: A closure to retrieve a two factor auth code from the user. 20 | /// - parameter closure: A closure that will be called when the login is completed, 21 | /// on failure a `SonarError`. 22 | public func login(getTwoFactorCode: @escaping (_ closure: @escaping (_ code: String?) -> Void) -> Void, 23 | closure: @escaping (Result) -> Void) 24 | { 25 | self.tracker.login(getTwoFactorCode: getTwoFactorCode) { result in 26 | closure(result) 27 | self.hold() 28 | } 29 | } 30 | 31 | /// Creates a new ticket into the bug tracker (needs authentication first). 32 | /// 33 | /// - parameter radar: The radar model with the information for the ticket. 34 | /// - parameter closure: A closure that will be called when the login is completed, on success it will 35 | /// contain a radar ID; on failure a `SonarError`. 36 | public func create(radar: Radar, closure: @escaping (Result) -> Void) { 37 | self.tracker.create(radar: radar) { result in 38 | closure(result) 39 | self.hold() 40 | } 41 | } 42 | 43 | /// Similar to `create` but logs the user in first. 44 | /// 45 | /// - parameter radar: The radar model with the information for the ticket. 46 | /// - parameter getTwoFactorCode: A closure to retrieve a two factor auth code from the user. 47 | /// - parameter closure: A closure that will be called when the login is completed, on success it 48 | /// will contain a radar ID; on failure a `SonarError`. 49 | public func loginThenCreate( 50 | radar: Radar, getTwoFactorCode: @escaping (_ closure: @escaping (_ code: String?) -> Void) -> Void, 51 | closure: @escaping (Result) -> Void) 52 | { 53 | self.tracker.login(getTwoFactorCode: getTwoFactorCode) { result in 54 | if case let .failure(error) = result { 55 | closure(.failure(error)) 56 | return 57 | } 58 | 59 | self.create(radar: radar, closure: closure) 60 | } 61 | } 62 | 63 | private func hold() {} 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/Radar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let kProductiOSID = 1 4 | private let kProductiTunesConnectID = 12 5 | 6 | public struct Radar { 7 | // The unique identifier on apple's radar platform. 8 | public var ID: Int? 9 | // The type of problem. 10 | public let classification: Classification 11 | // The product related to the report. 12 | public let product: Product 13 | // How often the problem occurs. 14 | public let reproducibility: Reproducibility 15 | // A short but descriptive sentence that summarizes the issue. 16 | public let title: String 17 | // A detailed description about the issue and include specific details to help the engineering 18 | // team understand the problem. 19 | public let description: String 20 | // The step by step process to reproduce the issue. 21 | public let steps: String 22 | // What you expected to see. 23 | public let expected: String 24 | // What you actually saw. 25 | public let actual: String 26 | // The circumstances where this does or does not occur. 27 | public let configuration: String 28 | // Product version and build number. 29 | public let version: String 30 | // Any other relevant notes not previously mentioned 31 | public let notes: String 32 | 33 | // The area in which the problem occurs (only applies for product=iOS). 34 | public let area: Area? 35 | // The identifier for your app located as part of General Information section (iTunes Connect only) 36 | public let applicationID: String? 37 | // Email address or username of the user experiencing the issue (iTunes Connect only) 38 | public let userID: String? 39 | // The attachments to send with the radar 40 | public let attachments: [Attachment] 41 | 42 | public init(classification: Classification, product: Product, reproducibility: Reproducibility, 43 | title: String, description: String, steps: String, expected: String, actual: String, 44 | configuration: String, version: String, notes: String, attachments: [Attachment], 45 | area: Area? = nil, applicationID: String? = nil, userID: String? = nil, ID: Int? = nil) 46 | { 47 | assert(area == nil || Area.areas(for: product).contains(where: { $0 == area! }), 48 | "The area passed must be be part of the product's areas") 49 | 50 | self.ID = ID 51 | self.classification = classification 52 | self.product = product 53 | self.reproducibility = reproducibility 54 | self.title = title 55 | self.description = description 56 | self.steps = steps 57 | self.expected = expected 58 | self.actual = actual 59 | self.configuration = configuration 60 | self.version = version 61 | self.notes = notes 62 | self.area = area 63 | self.applicationID = applicationID 64 | self.userID = userID 65 | self.attachments = attachments 66 | } 67 | } 68 | 69 | // MARK: - Body 70 | 71 | extension Radar { 72 | 73 | /// The composed body string using many of the components from the Radar model. 74 | var body: String { 75 | let baseTemplate = [ 76 | ("Summary", self.description), 77 | ("Steps to Reproduce", self.steps), 78 | ("Expected Results", self.expected), 79 | ("Actual Results", self.actual), 80 | ("Version", self.version), 81 | ("Notes", self.notes), 82 | ] 83 | 84 | let templates = [ 85 | kProductiTunesConnectID: [ 86 | ("Apple ID of the App", self.applicationID ?? ""), 87 | ("Apple ID of the User", self.userID ?? "") 88 | ], 89 | kProductiOSID: [ 90 | ("Area", self.area?.name ?? ""), 91 | ], 92 | ] 93 | 94 | let values = baseTemplate + (templates[self.product.appleIdentifier] ?? []) 95 | let body = values 96 | .map { "\($0):\r\n\($1)" } 97 | .joined(separator: "\r\n\r\n") 98 | return body + "\r\n\r\n" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/AppleRadarRouter.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | typealias Components = (path: String, method: HTTPMethod, headers: [String: String], 5 | data: Data?, parameters: [String: String]) 6 | 7 | /// Apple's radar request router. 8 | /// 9 | /// - accessToken: Fetch the access token to be sent in the `Radar-Authentication` header 10 | /// - accountInfo: Fetch the `myacinfo` cookie based on an apple ID and password 11 | /// - authorizeTwoFactor: The `Route` used to login with two factor auth. 12 | /// - create: The `Route` used to create a new radar. 13 | /// - sessionID: Fetch the `JSESSIONID` cookie using the previously set `myacinfo` cookie 14 | /// - viewProblem: The main apple's radar page. 15 | enum AppleRadarRouter { 16 | case accountInfo(appleID: String, password: String) 17 | case authorizeTwoFactor(code: String, scnt: String, sessionID: String) 18 | case accessToken 19 | case create(radar: Radar, token: String) 20 | case sessionID 21 | case viewProblem 22 | case uploadAttachment(radarID: Int, attachment: Attachment, token: String) 23 | 24 | fileprivate static let baseURL = URL(string: "https://bugreport.apple.com")! 25 | 26 | /// The request components including headers and parameters. 27 | var components: Components { 28 | switch self { 29 | case .viewProblem: 30 | return (path: "/problem/viewproblem", method: .get, headers: [:], data: nil, parameters: [:]) 31 | 32 | case .authorizeTwoFactor(let code, let scnt, let sessionID): 33 | let fullURL = "https://idmsa.apple.com/appleauth/auth/verify/trusteddevice/securitycode" 34 | let headers = [ 35 | "Accept": "application/json", 36 | "Content-Type": "application/json", 37 | "scnt": scnt, 38 | "X-Apple-App-Id": "21", 39 | "X-Apple-ID-Session-Id": sessionID, 40 | "X-Apple-Widget-Key": "16452abf721961a1728885bef033f28e", 41 | "X-Requested-With": "XMLHttpRequest", 42 | ] 43 | 44 | let JSON: [String: Any] = [ 45 | "securityCode": [ 46 | "code": code, 47 | ], 48 | ] 49 | 50 | let body = try! JSONSerialization.data(withJSONObject: JSON, options: []) 51 | return (path: fullURL, method: .post, headers: headers, data: body, parameters: [:]) 52 | 53 | case .accountInfo(let appleID, let password): 54 | let fullURL = "https://idmsa.apple.com/appleauth/auth/signin" 55 | let headers = [ 56 | "Accept": "application/json, text/javascript, */*; q=0.01", 57 | "Content-Type": "application/json", 58 | "X-Apple-App-Id": "21", 59 | "X-Apple-Widget-Key": "16452abf721961a1728885bef033f28e", 60 | "X-Requested-With": "XMLHttpRequest", 61 | ] 62 | 63 | let JSON: [String: Any] = [ 64 | "accountName": appleID, 65 | "password": password, 66 | "rememberMe": false, 67 | ] 68 | 69 | let body = try! JSONSerialization.data(withJSONObject: JSON, options: []) 70 | return (path: fullURL, method: .post, headers: headers, data: body, parameters: [:]) 71 | 72 | case .sessionID: 73 | return (path: "/", method: .get, headers: [:], data: nil, parameters: [:]) 74 | 75 | case .accessToken: 76 | let headers = ["Accept": "application/json, text/plain, */*"] 77 | return (path: "/developerUISignon", method: .get, headers: headers, data: nil, 78 | parameters: [:]) 79 | 80 | case .create(let radar, let token): 81 | let JSON: [String: Any] = [ 82 | "problemTitle": radar.title, 83 | "configIDPop": "", 84 | "configTitlePop": "", 85 | "configDescriptionPop": "", 86 | "configurationText": radar.configuration, 87 | "notes": radar.notes, 88 | "configurationSplit": "Configuration:\r\n", 89 | "configurationSplitValue": radar.configuration, 90 | "workAroundText": "", 91 | "descriptionText": radar.body, 92 | "problemAreaTypeCode": radar.area.map { String($0.appleIdentifier) } ?? "", 93 | "classificationCode": String(radar.classification.appleIdentifier), 94 | "reproducibilityCode": String(radar.reproducibility.appleIdentifier), 95 | "component": [ 96 | "ID": String(radar.product.appleIdentifier), 97 | "compName": radar.product.name, 98 | ], 99 | "draftID": "", 100 | "draftFlag": "", 101 | "versionBuild": radar.version, 102 | "desctextvalidate": radar.body, 103 | "stepstoreprvalidate": radar.steps, 104 | "experesultsvalidate": radar.expected, 105 | "actresultsvalidate": radar.actual, 106 | "addnotesvalidate": radar.notes, 107 | "hiddenFileSizeNew": "", 108 | "attachmentsValue": "\r\n\r\nAttachments:\r\n", 109 | "configurationFileCheck": "", 110 | "configurationFileFinal": "", 111 | ] 112 | 113 | let body = try! JSONSerialization.data(withJSONObject: JSON, options: []) 114 | let headers = [ 115 | "Referer": AppleRadarRouter.viewProblem.url.absoluteString, 116 | "Radar-Authentication": token, 117 | ] 118 | return (path: "/developerUI/problem/createNewDevUIProblem", method: .post, headers: headers, 119 | data: body, parameters: [:]) 120 | 121 | case .uploadAttachment(let radarID, let attachment, let token): 122 | let escapedName = attachment.filename 123 | .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? attachment.filename 124 | 125 | let headers = [ 126 | "Referer": AppleRadarRouter.viewProblem.url.absoluteString, 127 | "Radar-Authentication": token, 128 | ] 129 | 130 | return (path: "/problems/\(radarID)/attachments/\(escapedName)", method: .put, 131 | headers: headers, data: nil, parameters: [:]) 132 | } 133 | } 134 | } 135 | 136 | extension AppleRadarRouter: URLRequestConvertible { 137 | /// The URL that will be used for the request. 138 | var url: URL { 139 | return self.urlRequest!.url! 140 | } 141 | 142 | /// The request representation of the route including parameters and HTTP method. 143 | func asURLRequest() -> URLRequest { 144 | let (path, method, headers, data, parameters) = self.components 145 | let fullURL: URL 146 | if let url = URL(string: path), url.host != nil { 147 | fullURL = url 148 | } else { 149 | fullURL = AppleRadarRouter.baseURL.appendingPathComponent(path) 150 | } 151 | 152 | var request = URLRequest(url: fullURL) 153 | request.httpMethod = method.rawValue 154 | request.httpBody = data 155 | 156 | for (key, value) in headers { 157 | request.setValue(value, forHTTPHeaderField: key) 158 | } 159 | 160 | if data == nil { 161 | return try! URLEncoding().encode(request, with: parameters) 162 | } 163 | 164 | return request 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Sources/Sonar/APIs/AppleRadar.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | private let kRadarDowntimeURL = URL(string: "http://static.ips.apple.com/radar/web/downtime/index.html") 5 | 6 | final class AppleRadar: BugTracker { 7 | private let credentials: (appleID: String, password: String) 8 | private let manager: Alamofire.SessionManager 9 | private var token: String? 10 | 11 | /// - parameter appleID: Username to be used on `bugreport.apple.com` authentication. 12 | /// - parameter password: Password to be used on `bugreport.apple.com` authentication. 13 | init(appleID: String, password: String) { 14 | self.credentials = (appleID: appleID, password: password) 15 | 16 | let configuration = URLSessionConfiguration.default 17 | let cookies = HTTPCookieStorage.shared 18 | configuration.httpCookieStorage = cookies 19 | configuration.httpCookieAcceptPolicy = .always 20 | 21 | self.manager = Alamofire.SessionManager(configuration: configuration) 22 | } 23 | 24 | /// Login into radar by an apple ID and password. 25 | /// 26 | /// - parameter getTwoFactorCode: A closure to retrieve a two factor auth code from the user. 27 | /// - parameter closure: A closure that will be called when the login is completed, on success it 28 | /// will contain a list of `Product`s; on failure a `SonarError`. 29 | func login(getTwoFactorCode: @escaping (_ closure: @escaping (_ code: String?) -> Void) -> Void, 30 | closure: @escaping (Result) -> Void) 31 | { 32 | self.manager 33 | .request(AppleRadarRouter.accountInfo(appleID: credentials.appleID, 34 | password: credentials.password)) 35 | .validate() 36 | .responseString { [weak self] response in 37 | if let httpResponse = response.response, httpResponse.statusCode == 409 { 38 | getTwoFactorCode { code in 39 | if let code = code { 40 | self?.handleTwoFactorChallenge(code: code, headers: httpResponse.allHeaderFields, 41 | closure: closure) 42 | } else { 43 | closure(.failure(SonarError(message: "No 2 factor auth code provided"))) 44 | } 45 | } 46 | } else if case .success = response.result { 47 | self?.fetchAccessToken(closure: closure) 48 | } else { 49 | closure(.failure(SonarError.from(response))) 50 | } 51 | } 52 | } 53 | 54 | /// Creates a new ticket into apple's radar (needs authentication first). 55 | /// 56 | /// - parameter radar: The radar model with the information for the ticket. 57 | /// - parameter closure: A closure that will be called when the login is completed, on success it will 58 | /// contain a radar ID; on failure a `SonarError`. 59 | func create(radar: Radar, closure: @escaping (Result) -> Void) { 60 | guard let token = self.token else { 61 | closure(.failure(SonarError(message: "User is not logged in"))) 62 | return 63 | } 64 | 65 | let route = AppleRadarRouter.create(radar: radar, token: token) 66 | let (_, method, headers, body, _) = route.components 67 | let createMultipart = { (data: MultipartFormData) -> Void in 68 | data.append(body ?? Data(), withName: "hJsonScreenVal") 69 | } 70 | 71 | self.manager 72 | .upload(multipartFormData: createMultipart, to: route.url, method: method, headers: headers) 73 | { result in 74 | guard case let .success(upload, _, _) = result else { 75 | closure(.failure(.unknownError)) 76 | return 77 | } 78 | 79 | upload.validate().responseString { response in 80 | guard case let .success(value) = response.result else { 81 | closure(.failure(SonarError.from(response))) 82 | return 83 | } 84 | 85 | if let radarID = Int(value) { 86 | return self.uploadAttachments(radarID: radarID, attachments: radar.attachments, 87 | token: token, closure: closure) 88 | } 89 | 90 | if let json = jsonObject(from: value), json["isError"] as? Bool == true { 91 | let message = json["message"] as? String ?? "Unknown error occurred" 92 | closure(.failure(SonarError(message: message))) 93 | } else { 94 | closure(.failure(SonarError(message: "Invalid Radar ID received"))) 95 | } 96 | } 97 | } 98 | } 99 | 100 | // MARK: - Private Functions 101 | 102 | private func handleTwoFactorChallenge(code: String, headers: [AnyHashable: Any], 103 | closure: @escaping (Result) -> Void) 104 | { 105 | guard let sessionID = headers["X-Apple-ID-Session-Id"] as? String, 106 | let scnt = headers["scnt"] as? String else 107 | { 108 | closure(.failure(SonarError(message: "Missing Session-Id or scnt"))) 109 | return 110 | } 111 | 112 | self.manager 113 | .request(AppleRadarRouter.authorizeTwoFactor(code: code, scnt: scnt, sessionID: sessionID)) 114 | .validate() 115 | .responseString { [weak self] response in 116 | guard case .success = response.result else { 117 | closure(.failure(SonarError.from(response))) 118 | return 119 | } 120 | 121 | self?.manager 122 | .request(AppleRadarRouter.sessionID) 123 | .validate() 124 | .responseString { [weak self] response in 125 | if case .success = response.result { 126 | self?.fetchAccessToken(closure: closure) 127 | } else { 128 | closure(.failure(SonarError.from(response))) 129 | } 130 | } 131 | } 132 | } 133 | 134 | private func fetchAccessToken(closure: @escaping (Result) -> Void) { 135 | self.manager 136 | .request(AppleRadarRouter.sessionID) 137 | .validate() 138 | .response { [weak self] _ in 139 | self?.manager 140 | .request(AppleRadarRouter.accessToken) 141 | .validate() 142 | .responseJSON { [weak self] response in 143 | if case .success(let value) = response.result, 144 | let dictionary = value as? NSDictionary, 145 | let token = dictionary["accessToken"] as? String 146 | { 147 | self?.token = token 148 | closure(.success(())) 149 | } else if response.response?.url == kRadarDowntimeURL { 150 | closure(.failure(SonarError(message: "Radar appears to be down"))) 151 | } else { 152 | closure(.failure(SonarError.from(response))) 153 | } 154 | } 155 | } 156 | } 157 | 158 | private func uploadAttachments(radarID: Int, attachments: [Attachment], token: String, 159 | closure: @escaping (Result) -> Void) 160 | { 161 | if attachments.isEmpty { 162 | return closure(.success(radarID)) 163 | } 164 | 165 | var successful = true 166 | let group = DispatchGroup() 167 | 168 | for attachment in attachments { 169 | group.enter() 170 | 171 | let route = AppleRadarRouter.uploadAttachment(radarID: radarID, attachment: attachment, 172 | token: token) 173 | assert(route.components.data == nil, "The data is uploaded manually, not from the route") 174 | 175 | self.manager 176 | .upload(attachment.data, with: route) 177 | .validate(statusCode: [201]) 178 | .response { result in 179 | defer { 180 | group.leave() 181 | } 182 | 183 | successful = successful && result.response?.statusCode == 201 184 | } 185 | } 186 | 187 | group.notify(queue: .main) { 188 | if successful { 189 | closure(.success(radarID)) 190 | } else { 191 | closure(.failure(SonarError(message: "Failed to upload attachments"))) 192 | } 193 | } 194 | } 195 | } 196 | 197 | private func jsonObject(from string: String) -> [String: Any]? { 198 | guard let data = string.data(using: .utf8) else { 199 | return nil 200 | } 201 | 202 | return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] 203 | } 204 | -------------------------------------------------------------------------------- /Sources/Sonar/Models/ModelsData.swift: -------------------------------------------------------------------------------- 1 | extension Classification { 2 | public static let Security = Classification(appleIdentifier: 1, name: "Security") 3 | public static let Crash = Classification(appleIdentifier: 2, name: "Crash/Hang/Data Loss") 4 | public static let Power = Classification(appleIdentifier: 3, name: "Power") 5 | public static let Performance = Classification(appleIdentifier: 4, name: "Performance") 6 | public static let UI = Classification(appleIdentifier: 5, name: "UI/Usability") 7 | public static let SeriousBug = Classification(appleIdentifier: 7, name: "Serious Bug") 8 | public static let OtherBug = Classification(appleIdentifier: 8, name: "Other Bug") 9 | public static let Feature = Classification(appleIdentifier: 10, name: "Feature (New)") 10 | public static let Enhancement = Classification(appleIdentifier: 11, name: "Enhancement") 11 | 12 | public static let All: [Classification] = [ 13 | .Security, .Crash, .Power, .Performance, .UI, .SeriousBug, .OtherBug, .Feature, .Enhancement 14 | ] 15 | } 16 | 17 | extension Reproducibility { 18 | public static let Always = Reproducibility(appleIdentifier: 1, name: "Always") 19 | public static let Sometimes = Reproducibility(appleIdentifier: 2, name: "Sometimes") 20 | public static let Rarely = Reproducibility(appleIdentifier: 3, name: "Rarely") 21 | public static let Unable = Reproducibility(appleIdentifier: 4, name: "Unable") 22 | public static let DidntTry = Reproducibility(appleIdentifier: 5, name: "I didn't try") 23 | public static let NotApplicable = Reproducibility(appleIdentifier: 6, name: "Not Applicable") 24 | 25 | public static let All: [Reproducibility] = [ 26 | .Always, .Sometimes, .Rarely, .Unable, .DidntTry, .NotApplicable 27 | ] 28 | } 29 | 30 | extension Area { 31 | private init(_ appleIdentifier: Int, _ name: String) { 32 | self.appleIdentifier = appleIdentifier 33 | self.name = name 34 | } 35 | 36 | public static func areas(for product: Product) -> [Area] { 37 | switch product { 38 | case Product.iOS: 39 | return self.AlliOSAreas 40 | case Product.macOS: 41 | return self.AllmacOSAreas 42 | case Product.tvOS: 43 | return self.AlltvOSAreas 44 | case Product.watchOS: 45 | return self.AllwatchOSAreas 46 | default: 47 | return [] 48 | } 49 | } 50 | 51 | private static let AlliOSAreas: [Area] = [ 52 | Area(1, "Accelerate Framework"), Area(2, "Accessibility"), Area(3, "Accounts Framework"), 53 | Area(4, "App Store"), Area(5, "App Switcher"), Area(6, "ARKit"), Area(7, "Audio"), 54 | Area(8, "Audio Toolbox"), Area(9, "AVFoundation"), Area(10, "AVKit"), Area(11, "Battery Life"), 55 | Area(12, "Bluetooth"), Area(13, "Calendar"), Area(14, "CallKit"), Area(15, "CarPlay"), 56 | Area(16, "Cellular Service (Calls / Data)"), Area(17, "CFNetwork Framework"), Area(18, "CloudKit"), 57 | Area(19, "Contacts"), Area(20, "Contacts Framework"), Area(21, "Control Center"), 58 | Area(22, "Core Animation"), Area(23, "Core Bluetooth"), Area(24, "Core Data"), Area(25, "Core Foundation"), 59 | Area(26, "Core Graphics"), Area(27, "Core Image"), Area(28, "Core Location"), Area(29, "Core Media"), 60 | Area(30, "Core Motion"), Area(31, "Core Spotlight"), Area(32, "Core Telephony Framework"), 61 | Area(33, "Core Video"), Area(34, "EventKit"), Area(35, "External Accessory Framework"), 62 | Area(36, "FaceTime"), Area(37, "Foundation"), Area(38, "GameplayKit"), Area(39, "GLKit"), 63 | Area(40, "Health App"), Area(41, "HealthKit"), Area(42, "Home App"), Area(43, "Home Screen"), 64 | Area(44, "HomeKit"), Area(45, "iCloud"), Area(46, "Image I/O"), Area(47, "Intents Framework"), 65 | Area(48, "IOKit"), Area(49, "iPod Accessory Protocol (iAP)"), Area(50, "iTunes Connect"), 66 | Area(51, "iTunes Store"), Area(52, "Keyboard"), Area(53, "Local Authentication Framework"), 67 | Area(54, "Location Services"), Area(55, "Lock Screen"), Area(56, "Mail"), Area(57, "MapKit"), 68 | Area(58, "Maps"), Area(59, "MDM"), Area(60, "Media Player Framework"), Area(61, "Messages"), 69 | Area(62, "Messages Framework"), Area(63, "Metal"), Area(64, "Model I/O"), 70 | Area(65, "Multipeer Connectivity Framework"), Area(66, "Music"), Area(67, "Network Extensions Framework"), 71 | Area(68, "Notes"), Area(69, "Notification Center"), Area(70, "NotificationCenter Framework"), 72 | Area(71, "Notifications"), Area(72, "PassKit"), Area(73, "Phone App"), Area(74, "Photos"), 73 | Area(75, "Photos Framework"), Area(76, "Profiles"), Area(77, "PushKit"), Area(78, "QuickLook Framework"), 74 | Area(79, "Reminders"), Area(80, "ReplayKit"), Area(81, "Safari"), Area(82, "Safari Services"), 75 | Area(83, "SceneKit"), Area(84, "Security Framework"), Area(85, "Setup Assistant"), 76 | Area(86, "Simulator"), Area(87, "Siri"), Area(88, "Social Framework"), Area(89, "Software Update"), 77 | Area(90, "Spotlight"), Area(91, "SpriteKit"), Area(92, "StoreKit"), Area(93, "System Slow/Unresponsive"), 78 | Area(94, "SystemConfiguration Framework"), Area(95, "TouchID"), Area(96, "UIKit"), 79 | Area(97, "UserNotifications Framework"), Area(98, "VPN"), Area(99, "Wallet"), Area(100, "WebKit"), 80 | Area(101, "Wi-Fi"), Area(102, "Xcode"), Area(103, "Something not on this list"), 81 | ] 82 | 83 | private static let AllmacOSAreas: [Area] = [ 84 | Area(1, "Accessibility"), Area(2, "Airplay"), Area(3, "APNS"), Area(4, "App Store"), 85 | Area(5, "AppKit"), Area(6, "Automation"), Area(7, "Battery"), Area(8, "Bluetooth"), 86 | Area(9, "Calendar"), Area(10, "Contacts"), Area(11, "Core Graphics"), Area(12, "Developer Web Site"), 87 | Area(13, "Disk Utility"), Area(14, "Dock"), Area(15, "Documentation"), Area(16, "FaceTime"), 88 | Area(17, "Final Cut Pro"), Area(18, "Finder"), Area(19, "Graphics & Imaging"), 89 | Area(20, "iBooks / Author"), Area(21, "iCloud / CloudKit"), Area(22, "Installation"), 90 | Area(23, "iTunes"), Area(24, "iTunes Connect"), Area(25, "Java"), 91 | Area(26, "Keyboards, mice and trackpads"), Area(27, "Keynote, Numbers & Pages"), 92 | Area(28, "Launchpad"), Area(29, "Localization"), Area(30, "Logic Pro X"), Area(31, "Mail"), 93 | Area(32, "Maps"), Area(33, "Menu Bar"), Area(34, "Messages"), Area(35, "Mission Control"), 94 | Area(36, "Networking"), Area(37, "News Publisher"), Area(38, "Notes"), Area(39, "Numbers"), 95 | Area(40, "Performance (slow, hung"), Area(41, "Photos"), Area(42, "Preferences"), Area(43, "Preview"), 96 | Area(44, "Printing & Faxing"), Area(45, "Reminders"), Area(46, "Safari"), Area(47, "App Sandbox"), 97 | Area(48, "Screen Saver & desktop"), Area(49, "Server (macOS Server"), Area(50, "Setup Assistant"), 98 | Area(51, "Software Update"), Area(52, "Spaces"), Area(53, "Spotlight"), Area(54, "Swift"), 99 | Area(55, "Terminal"), Area(56, "Time Machine"), Area(57, "Wi-Fi"), Area(58, "Xcode"), 100 | Area(59, "Something not on this list"), 101 | ] 102 | 103 | private static let AlltvOSAreas: [Area] = [ 104 | Area(1, "Accessibility"), Area(2, "Accounts"), Area(3, "AirPlay"), Area(4, "App Store"), 105 | Area(5, "App Switcher"), Area(6, "AV Playback"), Area(7, "Bluetooth"), 106 | Area(8, "Device Management / Profiles"), Area(9, "Display"), Area(10, "Home Screen"), 107 | Area(11, "Movies / TV Shows"), Area(12, "Music"), Area(13, "Networking"), Area(14, "Photos"), 108 | Area(15, "Remote.app Support"), Area(16, "Setup (includes Tap Setup"), Area(17, "Screensaver"), 109 | Area(18, "Search"), Area(19, "Settings"), Area(20, "Siri"), Area(21, "Software Update"), 110 | Area(22, "SSO"), Area(23, "System, General"), Area(24, "TV App"), Area(25, "TVML Apps"), 111 | Area(26, "tvOS SDK"), 112 | ] 113 | 114 | private static let AllwatchOSAreas: [Area] = [ 115 | Area(1, "Accessibility"), Area(2, "Activity"), Area(3, "Battery Life"), Area(4, "Bluetooth"), 116 | Area(5, "Calendar"), Area(6, "Contacts"), Area(7, "Control Center"), Area(8, "Dock"), 117 | Area(9, "HealthKit"), Area(10, "HomeKit"), Area(11, "Maps"), Area(12, "Messages"), Area(13, "Music"), 118 | Area(14, "Notification Center"), Area(15, "Notifications"), Area(16, "Setup and Pairing"), 119 | Area(17, "Simulator"), Area(18, "Siri"), Area(19, "Syncing"), Area(20, "Wallet"), 120 | Area(21, "Watch Faces"), Area(22, "WatchKit"), Area(23, "Workout"), Area(24, "Xcode"), 121 | ] 122 | } 123 | 124 | extension Product { 125 | private init(_ appleIdentifier: Int, _ name: String, _ category: String) { 126 | self.appleIdentifier = appleIdentifier 127 | self.category = category 128 | self.name = name 129 | } 130 | 131 | public static let iOS = Product(579020, "iOS + SDK", "OS and Development") 132 | public static let macOS = Product(137701, "macOS + SDK", "OS and Development") 133 | public static let macOSServer = Product(84100, "Server", "OS and Development") 134 | public static let tvOS = Product(660932, "tvOS + SDK", "OS and Development") 135 | public static let watchOS = Product(645251, "watchOS + SDK", "OS and Development") 136 | public static let DeveloperTools = Product(175326, "Developer Tools", "OS and Development") 137 | public static let Documentation = Product(183045, "Documentation", "OS and Development") 138 | public static let iTunesConnect = Product(500515, "iTunes Connect", "OS and Development") 139 | public static let ParallaxPreviewer = Product(720650, "Parallax Previewer", "OS and Development") 140 | public static let SampleCode = Product(205728, "Sample Code", "OS and Development") 141 | public static let TechNote = Product(385563, "Tech Note/Q&A", "OS and Development") 142 | public static let iBooks = Product(571983, "iBooks", "Applications and Software") 143 | public static let iCloud = Product(458288, "iCloud", "Applications and Software") 144 | public static let iLife = Product(445858, "iLife", "Applications and Software") 145 | public static let iTunes = Product(430173, "iTunes", "Applications and Software") 146 | public static let iWork = Product(372025, "iWork", "Applications and Software") 147 | public static let Mail = Product(372031, "Mail", "Applications and Software") 148 | public static let ProApps = Product(175412, "Pro Apps", "Applications and Software") 149 | public static let QuickTime = Product(84201, "QuickTime", "Applications and Software") 150 | public static let Safari = Product(175305, "Safari", "Applications and Software") 151 | public static let SafariBeta = Product(697770, "Safari Technology Preview", "Applications and Software") 152 | public static let Siri = Product(750751, "Siri", "Applications and Software") 153 | public static let SwiftPlaygrounds = Product(743970, "Swift Playgrounds", "Applications and Software") 154 | public static let AppleTV = Product(430025, "Apple TV", "Hardware") 155 | public static let iPad = Product(375383, "iPad", "Hardware") 156 | public static let iPhone = Product(262954, "iPhone/iPod touch", "Hardware") 157 | public static let iPod = Product(185585, "iPod", "Hardware") 158 | public static let Mac = Product(213680, "Mac", "Hardware") 159 | public static let Printing = Product(213679, "Printing/Fax", "Hardware") 160 | public static let OtherHardware = Product(657117, "Other Hardware", "Hardware") 161 | public static let CarPlayAccessoryCert = Product(571212, "CarPlay Accessory Certification", "Hardware") 162 | public static let HomeKitAccessoryCert = Product(601986, "HomeKit Accessory Certification", "Hardware") 163 | public static let Accessibility = Product(437784, "Accessibility", "Other") 164 | public static let AppStore = Product(251822, "App Store", "Other") 165 | public static let MacAppStore = Product(430023, "Mac App Store", "Other") 166 | public static let BugReporter = Product(242322, "Bug Reporter", "Other") 167 | public static let iAdNetwork = Product(445860, "iAd Network", "Other") 168 | public static let iAdProducer = Product(446084, "iAd Producer", "Other") 169 | public static let Java = Product(84060, "Java", "Other") 170 | public static let Other = Product(20206, "Other", "Other") 171 | 172 | public static let All: [Product] = [ 173 | .iOS, .macOS, .macOSServer, .tvOS, .watchOS, 174 | .DeveloperTools, .Documentation, .iTunesConnect, .ParallaxPreviewer, .SampleCode, .TechNote, 175 | .iBooks, .iCloud, .iLife, .iTunes, .iWork, .Mail, .ProApps, .QuickTime, .Safari, .SafariBeta, .Siri, 176 | .SwiftPlaygrounds, .AppleTV, .iPad, .iPhone, .iPod, .Mac, .Printing, .OtherHardware, 177 | .CarPlayAccessoryCert, .HomeKitAccessoryCert, .Accessibility, .AppStore, .MacAppStore, .BugReporter, 178 | .iAdNetwork, .iAdProducer, .Java, .Other, 179 | ] 180 | } 181 | --------------------------------------------------------------------------------