15 Commits

Author SHA1 Message Date
macmade f1c82ab2a7 1.2.0-49 2021-10-08 18:57:43 +02:00
macmade da1f84a271 Preparing for 1.2.0 2021-10-08 18:56:58 +02:00
macmade 650adecff7 Update for Xcode 13. 2021-10-08 18:56:36 +02:00
macmade 44b367c830 Added support for provider short names, for Apple IDs associated with multiple teams. 2021-10-08 18:55:44 +02:00
macmade 41d88fa4ec Swift cleanup 2021-10-08 18:15:41 +02:00
macmade 45c1301a8c Swift cleanup 2021-10-08 18:12:56 +02:00
macmade 0a113b5f2d README 2021-10-08 17:13:01 +02:00
macmade 0029c93dd8 GItHub actions + sponsors 2021-10-08 17:09:42 +02:00
macmade 1b673784a0 Code of conduct 2021-10-08 17:08:21 +02:00
macmade 374caf14c0 License 2021-10-08 17:08:13 +02:00
macmade 87f051a3a1 1.1.2-39 2021-10-08 17:05:15 +02:00
macmade 6ffa0a951d Preparing for 1.1.2 2021-10-08 17:02:48 +02:00
macmade 8a0489103a Reading history from all pages 2021-10-08 17:02:01 +02:00
macmade 18debc3ed3 Better error reporting. Fixed altool location for latest Xcode versions. 2021-10-08 16:42:09 +02:00
macmade 7b6f1db509 Travis 2020-11-20 06:36:25 +01:00
18 changed files with 525 additions and 255 deletions
+1
View File
@@ -0,0 +1 @@
github: macmade
+26
View File
@@ -0,0 +1,26 @@
name: ci-mac
on: [push]
jobs:
ci:
runs-on: macos-latest
strategy:
matrix:
run-config:
- { scheme: 'Notarize', configuration: 'Debug', project: 'Notarize.xcodeproj', build: 1, analyze: 1, test: 0, info: 1, destination: 'platform=macOS' }
- { scheme: 'Notarize', configuration: 'Release', project: 'Notarize.xcodeproj', build: 1, analyze: 1, test: 0, info: 1, destination: 'platform=macOS' }
steps:
- uses: actions/checkout@v1
with:
submodules: 'recursive'
- uses: macmade/action-xcodebuild@v1.0.0
- uses: macmade/action-slack@v1.0.0
if: ${{ always() }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
with:
channel: '#ci'
status: ${{ job.status }}
title: ${{ matrix.run-config[ 'scheme' ] }} - ${{ matrix.run-config[ 'configuration' ] }}
+1 -1
View File
@@ -1,6 +1,6 @@
language: objective-c
xcode_project: Notarize.xcodeproj
xcode_scheme: Notarize
osx_image: xcode10
osx_image: xcode12
script:
- xcodebuild build -project Notarize.xcodeproj -scheme Notarize
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
xs-labs.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Jean-David Gadina - www.xs-labs.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+3 -3
View File
@@ -376,7 +376,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1220;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "XS-Labs";
TargetAttributes = {
059A0B4621905010004E1D89 = {
@@ -563,7 +563,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.1.1;
MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.xs-labs.Notarize";
PRODUCT_NAME = "$(TARGET_NAME)";
};
@@ -577,7 +577,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.1.1;
MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.xs-labs.Notarize";
PRODUCT_NAME = "$(TARGET_NAME)";
};
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+93 -50
View File
@@ -26,74 +26,110 @@ import Foundation
class ALTool
{
private var username: String
private var password: String
private var username: String
private var password: String
private var providerShortName: String?
class func isAvailable() -> Bool
{
let out = try? ALTool.run( arguments: [ "--help" ] )
guard let out = try? ALTool.run( arguments: [ "--help" ] ) else
{
return false
}
return out?.count ?? 0 > 0
return out.stdout.count > 0 || out.stderr.count > 0
}
init( username: String, password: String )
init( username: String, password: String, providerShortName: String? )
{
self.username = username
self.password = password
self.username = username
self.password = password
self.providerShortName = providerShortName
}
func checkPassword() throws
{
do
{
let _ = try ALTool.run( arguments: [ "--notarization-history", "-u", self.username, "-p", self.password, "--output-format", "xml" ] )
}
catch let e as NSError
{
throw e
}
let _ = try ALTool.run( arguments: self.arguments( [ "--notarization-history", "0" ] ) + [ "--output-format", "xml" ] )
}
func notarizationHistory() throws -> String?
func notarizationHistory( page: Int64 ) throws -> String?
{
do
{
let out = try ALTool.run( arguments: [ "--notarization-history", "-u", self.username, "-p", self.password, "--output-format", "xml" ] )
return out.trimmingCharacters( in: NSCharacterSet.whitespacesAndNewlines )
}
catch let e as NSError
{
throw e
}
let out = try ALTool.run( arguments: self.arguments( [ "--notarization-history", "\( page )" ] ) + [ "--output-format", "xml" ] )
return out.stdout.trimmingCharacters( in: NSCharacterSet.whitespacesAndNewlines )
}
func notarizationInfo( for uuid: String ) throws -> String?
{
do
{
let out = try ALTool.run( arguments: [ "--notarization-info", uuid, "-u", self.username, "-p", self.password, "--output-format", "xml" ] )
return out.trimmingCharacters( in: NSCharacterSet.whitespacesAndNewlines )
}
catch let e as NSError
{
throw e
}
let out = try ALTool.run( arguments: self.arguments( [ "--notarization-info", uuid ] ) + [ "--output-format", "xml" ] )
return out.stdout.trimmingCharacters( in: NSCharacterSet.whitespacesAndNewlines )
}
private class func run( arguments: [ String ] ) throws -> String
private func arguments( _ arguments: [ String ] ) -> [ String ]
{
var args = [ "altool" ]
var arguments = arguments
args.append( contentsOf: arguments )
arguments.append( "-u" )
arguments.append( self.username )
arguments.append( "-p" )
arguments.append( self.password )
if let name = self.providerShortName
{
arguments.append( "--asc-provider" )
arguments.append( name )
}
return arguments
}
private class var executablePath: String?
{
let pipe = Pipe()
let process = Process()
process.launchPath = "/usr/bin/xcrun"
process.arguments = args
process.arguments = [ "-f", "altool" ]
process.standardOutput = pipe
do
{
try process.run()
}
catch let e as NSError
{
Swift.print( e )
return nil
}
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let out = String( bytes: data, encoding: .utf8 ) else
{
return nil
}
return out.trimmingCharacters( in: .whitespacesAndNewlines )
}
private class func run( arguments: [ String ] ) throws -> ( stdout: String, stderr: String )
{
guard let exec = ALTool.executablePath else
{
throw NSError( domain: NSCocoaErrorDomain, code: -1, userInfo: [ NSLocalizedDescriptionKey : "altool executable not found" ] )
}
let pipeOut = Pipe()
let pipeErr = Pipe()
let process = Process()
process.launchPath = exec
process.arguments = arguments
process.standardOutput = pipeOut
process.standardError = pipeErr
do
{
try process.run()
@@ -107,26 +143,33 @@ class ALTool
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let dataOut = pipeOut.fileHandleForReading.readDataToEndOfFile()
let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile()
guard let out = String( bytes: data, encoding: .utf8 ) else
guard let out = String( bytes: dataOut, encoding: .utf8 ),
let err = String( bytes: dataErr, encoding: .utf8 )
else
{
throw NSError( domain: NSCocoaErrorDomain, code: -1, userInfo: [ NSLocalizedDescriptionKey : "No data received from altool" ] )
}
if let data = out.data( using: .utf8 )
{
if let dict = try? PropertyListSerialization.propertyList( from: data, options: [], format: nil ) as? NSDictionary
if let dict = try? PropertyListSerialization.propertyList( from: data, options: [], format: nil ) as? [ AnyHashable : Any ]
{
if let errors = dict.object( forKey: "product-errors") as? NSArray
if let errors = dict[ "product-errors" ] as? [ Any ]
{
if errors.count > 0
if errors.count > 0, let errorDict = errors[ 0 ] as? [ AnyHashable : Any ]
{
if let errorDict = errors.object( at: 0 ) as? NSDictionary
let code = errorDict[ "code" ] as? NSNumber ?? NSNumber( integerLiteral: 0 )
let message = errorDict[ "message" ] as? String ?? "Unknown error"
if let info = errorDict[ "userInfo" ] as? [ AnyHashable : Any ], let failure = info[ "NSLocalizedFailureReason" ] as? String
{
throw NSError( domain: NSCocoaErrorDomain, code: code.intValue, userInfo: [ NSLocalizedDescriptionKey : message, NSLocalizedRecoverySuggestionErrorKey : failure ] )
}
else
{
let code = errorDict.object( forKey: "code" ) as? NSNumber ?? NSNumber( integerLiteral: 0 )
let message = errorDict.object( forKey: "message" ) as? NSString ?? "Unknown error" as NSString
throw NSError( domain: NSCocoaErrorDomain, code: code.intValue, userInfo: [ NSLocalizedDescriptionKey : "Error", NSLocalizedRecoverySuggestionErrorKey : message ] )
}
}
@@ -134,6 +177,6 @@ class ALTool
}
}
return out
return ( stdout: out, stderr: err )
}
}
+13 -10
View File
@@ -26,16 +26,18 @@ import Cocoa
@objc class Account: NSObject
{
@objc public private( set ) dynamic var username: String
@objc public private( set ) dynamic var password: String?
@objc public private( set ) dynamic var useKeychain: Bool
@objc public private( set ) dynamic var username: String
@objc public private( set ) dynamic var password: String?
@objc public private( set ) dynamic var providerShortName: String?
@objc public private( set ) dynamic var useKeychain: Bool
private static var sessionPasswords = [ String : String ]()
init( username: String, useKeychain: Bool )
init( username: String, useKeychain: Bool, providerShortName: String? )
{
self.username = username
self.useKeychain = useKeychain
self.username = username
self.useKeychain = useKeychain
self.providerShortName = providerShortName
guard let bundleID = Bundle.main.bundleIdentifier else
{
@@ -53,11 +55,12 @@ import Cocoa
}
}
init( username: String, password: String, useKeychain: Bool )
init( username: String, password: String, useKeychain: Bool, providerShortName: String? )
{
self.username = username
self.password = password
self.useKeychain = useKeychain
self.username = username
self.password = password
self.useKeychain = useKeychain
self.providerShortName = providerShortName
guard let bundleID = Bundle.main.bundleIdentifier else
{
@@ -26,10 +26,11 @@ import Cocoa
class AccountWindowController: NSWindowController
{
@objc public dynamic var username = ""
@objc public private( set ) dynamic var password = ""
@objc public private( set ) dynamic var keepInKeychain = false
@objc public private( set ) dynamic var loading = false
@objc public dynamic var username = ""
@objc public dynamic var providerShortName = ""
@objc public private( set ) dynamic var password = ""
@objc public private( set ) dynamic var keepInKeychain = false
@objc public private( set ) dynamic var loading = false
override var windowNibName: NSNib.Name?
{
@@ -52,7 +53,7 @@ class AccountWindowController: NSWindowController
return
}
let altool = ALTool( username: self.username, password: self.password )
let altool = ALTool( username: self.username, password: self.password, providerShortName: self.providerShortName.count > 0 ? self.providerShortName : nil )
self.loading = true
+16 -44
View File
@@ -26,68 +26,40 @@ import Cocoa
@objc class HistoryItem: NSObject
{
@objc public dynamic var date: NSDate
@objc public dynamic var date: Date
@objc public dynamic var uuid: String
@objc public dynamic var success: Bool
@objc public dynamic var status: Int
@objc public dynamic var message: String
@objc public dynamic var logURL: String?
class func ItemsFromDictionary( dict: NSDictionary? ) -> [ HistoryItem ]
class func ItemsFromDictionary( dict: [ AnyHashable : Any ]? ) -> [ HistoryItem ]
{
guard let history = dict?.object( forKey: "notarization-history" ) as? NSDictionary else
guard let history = dict?[ "notarization-history" ] as? [ AnyHashable : Any ],
let items = history[ "items" ] as? [ Any ]
else
{
return []
}
guard let items = history.object( forKey: "items" ) as? NSArray else
return items.compactMap
{
return []
$0 as? [ AnyHashable : Any ]
}
var objects = [ HistoryItem ]()
for o in items
.compactMap
{
guard let item = o as? NSDictionary else
{
continue
}
guard let historyItem = HistoryItem( dict: item ) else
{
continue
}
objects.append( historyItem )
HistoryItem( dict: $0 )
}
return objects
}
init?( dict: NSDictionary )
init?( dict: [ AnyHashable : Any ] )
{
guard let date = dict.object( forKey: "Date" ) as? NSDate else
{
return nil
}
guard let uuid = dict.object( forKey: "RequestUUID" ) as? String else
{
return nil
}
guard let status = dict.object( forKey: "Status" ) as? String else
{
return nil
}
guard let code = dict.object( forKey: "Status Code" ) as? NSNumber else
{
return nil
}
guard let message = dict.object( forKey: "Status Message" ) as? String else
guard let date = dict[ "Date" ] as? Date,
let uuid = dict[ "RequestUUID" ] as? String,
let status = dict[ "Status" ] as? String,
let code = dict[ "Status Code" ] as? NSNumber,
let message = dict[ "Status Message" ] as? String
else
{
return nil
}
+88 -68
View File
@@ -82,12 +82,9 @@ class HistoryViewController: NSViewController, NSTableViewDelegate, NSTableViewD
@IBAction func showInfo( _ sender: Any? )
{
guard let item = sender as? HistoryItem else
{
return
}
guard let url = URL( string: item.logURL ?? "" ) else
guard let item = sender as? HistoryItem,
let url = URL( string: item.logURL ?? "" )
else
{
return
}
@@ -134,39 +131,87 @@ class HistoryViewController: NSViewController, NSTableViewDelegate, NSTableViewD
DispatchQueue.global( qos: .userInitiated ).async
{
let altool = ALTool( username: account.username, password: password )
let xml = try? altool.notarizationHistory()
if let xmlData = xml?.data( using: .utf8 )
do
{
if let history = try? PropertyListSerialization.propertyList( from: xmlData, options: [], format: nil ) as? NSDictionary
let items = try self.loadHistory( username: account.username, password: password, providerShortName: account.providerShortName )
DispatchQueue.main.async
{
let items = HistoryItem.ItemsFromDictionary( dict: history )
DispatchQueue.main.async
items.forEach
{
items.forEach
o in
if self.items.contains( o ) == false
{
o in
if self.items.contains( o ) == false
{
self.items.insert( o )
}
self.items.insert( o )
}
}
self.loading = false
self.refreshing = false
}
}
DispatchQueue.main.async
catch let error
{
self.loading = false
self.refreshing = false
if userInitiated
{
DispatchQueue.main.async
{
let alert = NSAlert( error: error )
if let window = self.view.window
{
alert.beginSheetModal( for: window, completionHandler: nil )
}
else
{
alert.runModal()
}
self.loading = false
self.refreshing = false
}
}
else
{
print( error )
}
}
}
}
}
private func loadHistory( username: String, password: String, providerShortName: String? ) throws -> [ HistoryItem ]
{
var items = [ HistoryItem ]()
let altool = ALTool( username: username, password: password, providerShortName: providerShortName )
var page = Int64( 0 )
repeat
{
let xml = try altool.notarizationHistory( page: page )
page = -1
if let xmlData = xml?.data( using: .utf8 )
{
if let history = try? PropertyListSerialization.propertyList( from: xmlData, options: [], format: nil ) as? [ AnyHashable : Any ]
{
let current = HistoryItem.ItemsFromDictionary( dict: history )
items.append( contentsOf: current )
if current.count > 0, let history = history[ "notarization-history" ] as? [ AnyHashable : Any ], let next = history[ "next-page" ] as? Int64
{
page = next
}
}
}
}
while page >= 0
return items
}
private func getInfo()
{
DispatchQueue.main.async
@@ -180,45 +225,26 @@ class HistoryViewController: NSViewController, NSTableViewDelegate, NSTableViewD
DispatchQueue.global( qos: .userInitiated ).async
{
guard let account = self.account else
{
return
}
guard let password = account.password else
guard let account = self.account,
let password = account.password
else
{
return
}
let items = DispatchQueue.main.sync { return self.items }
let altool = ALTool( username: account.username, password: password )
let altool = ALTool( username: account.username, password: password, providerShortName: account.providerShortName )
let group = DispatchGroup()
for item in items.filter( { o in o.logURL == nil } )
items.filter( { o in o.logURL == nil } ).forEach
{
DispatchQueue.global( qos: .userInitiated ).async( group: group )
item in DispatchQueue.global( qos: .userInitiated ).async( group: group )
{
guard let xml = try? altool.notarizationInfo( for: item.uuid ) else
{
return
}
guard let xmlData = xml.data( using: .utf8 ) else
{
return
}
guard let info = try? PropertyListSerialization.propertyList( from: xmlData, options: [], format: nil ) as? NSDictionary else
{
return
}
guard let notarization = info[ "notarization-info" ] as? NSDictionary else
{
return
}
guard let url = notarization[ "LogFileURL" ] as? String else
guard let xml = try? altool.notarizationInfo( for: item.uuid ),
let xmlData = xml.data( using: .utf8 ),
let info = try? PropertyListSerialization.propertyList( from: xmlData, options: [], format: nil ) as? [ AnyHashable : Any ],
let notarization = info[ "notarization-info" ] as? [ AnyHashable : Any ],
let url = notarization[ "LogFileURL" ] as? String else
{
return
}
@@ -249,22 +275,16 @@ class HistoryViewController: NSViewController, NSTableViewDelegate, NSTableViewD
self.add = AccountWindowController()
guard let add = self.add else
guard let add = self.add,
let account = self.account,
let sheet = add.window
else
{
return
}
guard let account = self.account else
{
return
}
guard let sheet = add.window else
{
return
}
add.username = account.username
add.username = account.username
add.providerShortName = account.providerShortName ?? ""
self.view.window?.beginSheet( sheet )
{
@@ -287,7 +307,7 @@ class HistoryViewController: NSViewController, NSTableViewDelegate, NSTableViewD
return
}
let account = Account( username: add.username, password: add.password, useKeychain: add.keepInKeychain )
let account = Account( username: add.username, password: add.password, useKeychain: add.keepInKeychain, providerShortName: add.providerShortName.count > 0 ? add.providerShortName : nil )
Preferences.shared.addAccount( account );
+7 -13
View File
@@ -101,12 +101,9 @@ class MainWindowController: NSWindowController
@IBAction func refresh( _ sender: Any? )
{
guard let account = self.accountsController.selectedObjects.first as? Account else
{
return
}
guard let controller = self.controllers[ account.username ] else
guard let account = self.accountsController.selectedObjects.first as? Account,
let controller = self.controllers[ account.username ]
else
{
return
}
@@ -149,7 +146,7 @@ class MainWindowController: NSWindowController
return
}
let account = Account( username: add.username, password: add.password, useKeychain: add.keepInKeychain )
let account = Account( username: add.username, password: add.password, useKeychain: add.keepInKeychain, providerShortName: add.providerShortName.count > 0 ? add.providerShortName : nil )
Preferences.shared.addAccount( account );
}
@@ -157,12 +154,9 @@ class MainWindowController: NSWindowController
@IBAction func removeAccount( _ sender: Any? )
{
guard let account = self.accountsController.selectedObjects.first as? Account else
{
return
}
guard let window = self.window else
guard let account = self.accountsController.selectedObjects.first as? Account,
let window = self.window
else
{
return
}
+41 -19
View File
@@ -27,7 +27,7 @@ import Cocoa
@objc public class Preferences: NSObject
{
@objc public dynamic var lastStart: Date?
@objc public dynamic var accounts: NSDictionary?
@objc public dynamic var accounts: [ AnyHashable : Any ]?
@objc public static let shared = Preferences()
@@ -83,46 +83,68 @@ import Cocoa
func addAccount( _ account: Account )
{
let accounts = self.accounts?.mutableCopy() as? NSMutableDictionary ?? NSMutableDictionary()
var accounts = self.accounts ?? [:]
accounts.setObject( NSNumber( booleanLiteral: account.useKeychain ), forKey: account.username as NSString )
if let name = account.providerShortName
{
accounts[ account.username ] =
[
"UseKeychain" : NSNumber( booleanLiteral: account.useKeychain ),
"ProviderShortName" : name
]
}
else
{
accounts[ account.username ] = NSNumber( booleanLiteral: account.useKeychain )
}
self.accounts = accounts.copy() as? NSDictionary
self.accounts = accounts
}
func removeAccount( _ account: Account )
{
guard let accounts = self.accounts?.mutableCopy() as? NSMutableDictionary else
guard var accounts = self.accounts else
{
return
}
accounts.removeObject( forKey: account.username as NSString )
accounts.removeValue( forKey: account.username )
if account.useKeychain, let bundleID = Bundle.main.bundleIdentifier
{
let _ = Keychain( keychain: nil ).delete( service: bundleID, account: account.username )
}
self.accounts = accounts.copy() as? NSDictionary
self.accounts = accounts
}
func getAccounts() -> [ Account ]
{
var accounts = [ Account ]()
for account in self.accounts ?? [ : ]
guard let accounts = self.accounts else
{
guard let username = account.key as? String else
{
continue
}
let useKeychain = ( account.value as? NSNumber )?.boolValue ?? false
accounts.append( Account( username: username, useKeychain: useKeychain ) )
return []
}
return accounts
return accounts.compactMap
{
guard let username = $0.key as? String else
{
return nil
}
if let info = $0.value as? [ AnyHashable : Any ]
{
let useKeychain = info[ "UseKeychain" ] as? NSNumber
let providerShortName = info[ "ProviderShortName" ] as? String
return Account( username: username, useKeychain: useKeychain?.boolValue ?? false, providerShortName: providerShortName )
}
else
{
let useKeychain = ( $0.value as? NSNumber )?.boolValue ?? false
return Account( username: username, useKeychain: useKeychain, providerShortName: nil )
}
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>34</string>
<string>49</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19162" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19162"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -15,23 +16,23 @@
<window title="Developer Account" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="579" height="253"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1177"/>
<rect key="contentRect" x="196" y="240" width="380" height="316"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1228"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="579" height="253"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="316"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="a22-UJ-tuT">
<rect key="frame" x="0.0" y="0.0" width="579" height="253"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="316"/>
<subviews>
<progressIndicator wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="Qao-Uu-KJq">
<rect key="frame" x="274" y="111" width="32" height="32"/>
<rect key="frame" x="284" y="142" width="32" height="32"/>
<connections>
<binding destination="-2" name="animate" keyPath="self.loading" id="MqU-UR-2Fv"/>
</connections>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MpK-jx-VzM">
<rect key="frame" x="217" y="74" width="146" height="17"/>
<rect key="frame" x="227" y="106" width="146" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Validating credentials..." id="ss1-SH-70u">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -54,10 +55,10 @@
</connections>
</customView>
<view wantsLayer="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NsU-Ic-SGu">
<rect key="frame" x="0.0" y="0.0" width="579" height="253"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="316"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7PZ-Wb-pip">
<rect key="frame" x="189" y="203" width="201" height="30"/>
<rect key="frame" x="200" y="267" width="201" height="29"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Developer Account" id="iAM-dE-gkd">
<font key="font" metaFont="systemThin" size="25"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -65,21 +66,21 @@
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="VVw-Xg-IkG">
<rect key="frame" x="12" y="192" width="555" height="5"/>
<rect key="frame" x="12" y="256" width="576" height="5"/>
</box>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="NPL-u9-5wt">
<rect key="frame" x="20" y="85" width="86" height="86"/>
<rect key="frame" x="20" y="85" width="150" height="150"/>
<constraints>
<constraint firstAttribute="height" constant="86" id="kZP-ac-Ymk"/>
<constraint firstAttribute="width" constant="86" id="oK7-kG-qoa"/>
<constraint firstAttribute="height" constant="150" id="kZP-ac-Ymk"/>
<constraint firstAttribute="width" constant="150" id="oK7-kG-qoa"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Accounts" id="wZ3-Xa-Unj"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="Accounts" id="wZ3-Xa-Unj"/>
</imageView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="uYp-eZ-ccf">
<rect key="frame" x="126" y="70" width="433" height="116"/>
<rect key="frame" x="170" y="69" width="410" height="181"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1aP-Yq-W83">
<rect key="frame" x="18" y="77" width="89" height="17"/>
<rect key="frame" x="28" y="143" width="56" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="AppleID:" id="aNg-45-EUW">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -87,7 +88,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Omp-wi-GXb">
<rect key="frame" x="113" y="74" width="300" height="22"/>
<rect key="frame" x="90" y="140" width="300" height="21"/>
<constraints>
<constraint firstAttribute="width" constant="300" id="EVw-XJ-WXh"/>
</constraints>
@@ -105,7 +106,7 @@
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="awz-8s-w8g">
<rect key="frame" x="18" y="45" width="89" height="17"/>
<rect key="frame" x="18" y="112" width="66" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Password:" id="pcl-Ub-Bft">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -113,7 +114,7 @@
</textFieldCell>
</textField>
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Xey-Nm-kVG">
<rect key="frame" x="113" y="42" width="300" height="22"/>
<rect key="frame" x="90" y="109" width="300" height="21"/>
<secureTextFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="akR-lk-HnN">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -131,7 +132,7 @@
</connections>
</secureTextField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="R9Q-HB-RzW">
<rect key="frame" x="111" y="18" width="221" height="18"/>
<rect key="frame" x="88" y="84" width="225" height="18"/>
<buttonCell key="cell" type="check" title="Remember password in keychain" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="OMx-SG-lf0">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -140,30 +141,70 @@
<binding destination="-2" name="value" keyPath="self.keepInKeychain" id="FDN-KL-VDJ"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ay5-Du-OV8">
<rect key="frame" x="25" y="59" width="59" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Provider:" id="A1o-DN-PFh">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="oL0-iz-W4Y">
<rect key="frame" x="90" y="56" width="300" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="gHp-5g-4pA">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="value" keyPath="self.providerShortName" id="wRU-e9-EIN">
<dictionary key="options">
<bool key="NSContinuouslyUpdatesValue" value="YES"/>
</dictionary>
</binding>
</connections>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CtI-ZF-hLh">
<rect key="frame" x="88" y="20" width="304" height="28"/>
<textFieldCell key="cell" controlSize="small" selectable="YES" title="Optional: use this if your Apple ID account is also attached to other iTunes providers." id="uVO-rx-XWl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="R9Q-HB-RzW" secondAttribute="trailing" constant="103" id="0r4-pv-6PB"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="R9Q-HB-RzW" secondAttribute="trailing" constant="20" id="0r4-pv-6PB"/>
<constraint firstItem="CtI-ZF-hLh" firstAttribute="leading" secondItem="oL0-iz-W4Y" secondAttribute="leading" id="2oL-64-yr0"/>
<constraint firstItem="oL0-iz-W4Y" firstAttribute="top" secondItem="R9Q-HB-RzW" secondAttribute="bottom" constant="8" symbolic="YES" id="73I-c0-JrD"/>
<constraint firstItem="ay5-Du-OV8" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uYp-eZ-ccf" secondAttribute="leading" constant="20" symbolic="YES" id="8JE-po-At3"/>
<constraint firstItem="Xey-Nm-kVG" firstAttribute="top" secondItem="Omp-wi-GXb" secondAttribute="bottom" constant="10" id="E1e-XK-MTa"/>
<constraint firstItem="Xey-Nm-kVG" firstAttribute="leading" secondItem="awz-8s-w8g" secondAttribute="trailing" constant="8" id="EVv-Fw-pZH"/>
<constraint firstAttribute="bottom" secondItem="R9Q-HB-RzW" secondAttribute="bottom" constant="20" id="FEu-m9-igb"/>
<constraint firstItem="Xey-Nm-kVG" firstAttribute="leading" secondItem="Omp-wi-GXb" secondAttribute="leading" id="Fd3-5p-TOc"/>
<constraint firstItem="R9Q-HB-RzW" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Xey-Nm-kVG" secondAttribute="leading" id="MJi-aJ-g9U"/>
<constraint firstItem="awz-8s-w8g" firstAttribute="leading" secondItem="uYp-eZ-ccf" secondAttribute="leading" constant="20" id="Pup-3F-Z4r"/>
<constraint firstItem="oL0-iz-W4Y" firstAttribute="trailing" secondItem="Xey-Nm-kVG" secondAttribute="trailing" id="GOR-tX-VGi"/>
<constraint firstItem="ay5-Du-OV8" firstAttribute="centerY" secondItem="oL0-iz-W4Y" secondAttribute="centerY" id="Gzo-Nc-JNL"/>
<constraint firstItem="R9Q-HB-RzW" firstAttribute="leading" secondItem="Xey-Nm-kVG" secondAttribute="leading" id="MJi-aJ-g9U"/>
<constraint firstItem="awz-8s-w8g" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uYp-eZ-ccf" secondAttribute="leading" constant="20" id="Pup-3F-Z4r"/>
<constraint firstItem="CtI-ZF-hLh" firstAttribute="top" secondItem="oL0-iz-W4Y" secondAttribute="bottom" constant="8" symbolic="YES" id="Pw2-fw-R6r"/>
<constraint firstItem="R9Q-HB-RzW" firstAttribute="top" secondItem="Xey-Nm-kVG" secondAttribute="bottom" constant="8" id="W4j-qe-92T"/>
<constraint firstAttribute="trailing" secondItem="Omp-wi-GXb" secondAttribute="trailing" constant="20" id="XJ1-Uy-3XR"/>
<constraint firstItem="Omp-wi-GXb" firstAttribute="leading" secondItem="1aP-Yq-W83" secondAttribute="trailing" constant="8" id="ah6-3F-2Mf"/>
<constraint firstAttribute="trailing" secondItem="CtI-ZF-hLh" secondAttribute="trailing" constant="20" id="bwB-WC-nra"/>
<constraint firstAttribute="bottom" secondItem="CtI-ZF-hLh" secondAttribute="bottom" constant="20" id="jlG-TG-nMp"/>
<constraint firstItem="Omp-wi-GXb" firstAttribute="top" secondItem="uYp-eZ-ccf" secondAttribute="top" constant="20" id="jzv-a3-0cg"/>
<constraint firstItem="1aP-Yq-W83" firstAttribute="leading" secondItem="uYp-eZ-ccf" secondAttribute="leading" constant="20" id="lqc-dV-rG1"/>
<constraint firstItem="1aP-Yq-W83" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uYp-eZ-ccf" secondAttribute="leading" constant="20" id="lqc-dV-rG1"/>
<constraint firstItem="oL0-iz-W4Y" firstAttribute="leading" secondItem="ay5-Du-OV8" secondAttribute="trailing" constant="8" symbolic="YES" id="n5j-dM-yiu"/>
<constraint firstItem="Xey-Nm-kVG" firstAttribute="trailing" secondItem="Omp-wi-GXb" secondAttribute="trailing" id="tLT-Cd-hiE"/>
<constraint firstItem="1aP-Yq-W83" firstAttribute="centerY" secondItem="Omp-wi-GXb" secondAttribute="centerY" id="xLM-DB-woi"/>
<constraint firstItem="oL0-iz-W4Y" firstAttribute="leading" secondItem="Xey-Nm-kVG" secondAttribute="leading" id="yNg-DO-bMQ"/>
<constraint firstItem="awz-8s-w8g" firstAttribute="centerY" secondItem="Xey-Nm-kVG" secondAttribute="centerY" id="zZM-uD-9kJ"/>
</constraints>
</customView>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="zrA-uE-xq3">
<rect key="frame" x="12" y="59" width="555" height="5"/>
<rect key="frame" x="12" y="58" width="576" height="5"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uCG-EV-sey">
<rect key="frame" x="506" y="13" width="59" height="32"/>
<rect key="frame" x="534" y="13" width="53" height="32"/>
<buttonCell key="cell" type="push" title="OK" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Ahu-P9-a1J">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -176,7 +217,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fGw-yj-aTk">
<rect key="frame" x="424" y="13" width="82" height="32"/>
<rect key="frame" x="460" y="13" width="76" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WbE-Xt-xvz">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -192,7 +233,7 @@ Gw
<constraints>
<constraint firstItem="VVw-Xg-IkG" firstAttribute="top" secondItem="7PZ-Wb-pip" secondAttribute="bottom" constant="8" id="2Yd-2Q-V2M"/>
<constraint firstItem="7PZ-Wb-pip" firstAttribute="centerX" secondItem="NsU-Ic-SGu" secondAttribute="centerX" id="CCT-zY-jhM"/>
<constraint firstItem="uYp-eZ-ccf" firstAttribute="leading" secondItem="NPL-u9-5wt" secondAttribute="trailing" constant="20" id="CXR-QV-Wrr"/>
<constraint firstItem="uYp-eZ-ccf" firstAttribute="leading" secondItem="NPL-u9-5wt" secondAttribute="trailing" id="CXR-QV-Wrr"/>
<constraint firstAttribute="trailing" secondItem="VVw-Xg-IkG" secondAttribute="trailing" constant="12" id="DHp-8P-rqS"/>
<constraint firstItem="NPL-u9-5wt" firstAttribute="leading" secondItem="NsU-Ic-SGu" secondAttribute="leading" constant="20" id="HmQ-nE-D8g"/>
<constraint firstItem="VVw-Xg-IkG" firstAttribute="leading" secondItem="NsU-Ic-SGu" secondAttribute="leading" constant="12" id="Jsw-ZW-s8y"/>
@@ -226,7 +267,7 @@ Gw
<constraint firstItem="NsU-Ic-SGu" firstAttribute="width" secondItem="EiT-Mj-1SZ" secondAttribute="width" id="ykL-cN-uKZ"/>
</constraints>
</view>
<point key="canvasLocation" x="177.5" y="-190.5"/>
<point key="canvasLocation" x="78" y="-191"/>
</window>
</objects>
<resources>
+6 -8
View File
@@ -1,14 +1,12 @@
Notarize
========
[![Build Status](https://img.shields.io/travis/macmade/Notarize.svg?branch=master&style=flat)](https://travis-ci.org/macmade/Notarize)
[![Issues](http://img.shields.io/github/issues/macmade/Notarize.svg?style=flat)](https://github.com/macmade/Notarize/issues)
![Status](https://img.shields.io/badge/status-active-brightgreen.svg?style=flat)
![License](https://img.shields.io/badge/license-mit-brightgreen.svg?style=flat)
[![Contact](https://img.shields.io/badge/contact-@macmade-blue.svg?style=flat)](https://twitter.com/macmade)
[![Donate-Patreon](https://img.shields.io/badge/donate-patreon-yellow.svg?style=flat)](https://patreon.com/macmade)
[![Donate-Gratipay](https://img.shields.io/badge/donate-gratipay-yellow.svg?style=flat)](https://www.gratipay.com/macmade)
[![Donate-Paypal](https://img.shields.io/badge/donate-paypal-yellow.svg?style=flat)](https://paypal.me/xslabs)
[![Build Status](https://img.shields.io/github/workflow/status/macmade/Notarize/ci-mac?label=macOS&logo=apple)](https://github.com/macmade/Notarize/actions/workflows/ci-mac.yaml)
[![Issues](http://img.shields.io/github/issues/macmade/Notarize.svg?logo=github)](https://github.com/macmade/Notarize/issues)
![Status](https://img.shields.io/badge/status-active-brightgreen.svg?logo=git)
![License](https://img.shields.io/badge/license-mit-brightgreen.svg?logo=open-source-initiative)
[![Contact](https://img.shields.io/badge/follow-@macmade-blue.svg?logo=twitter&style=social)](https://twitter.com/macmade)
[![Sponsor](https://img.shields.io/badge/sponsor-macmade-pink.svg?logo=github-sponsors&style=social)](https://github.com/sponsors/macmade)
### About