diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 303e2df..ef77ace 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -21,10 +21,11 @@ ED0F23831B87533A00AE40CD /* ListInstalled.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F23821B87533A00AE40CD /* ListInstalled.swift */; }; ED0F23851B87536A00AE40CD /* ListUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F23841B87536A00AE40CD /* ListUpdates.swift */; }; ED0F23871B87537200AE40CD /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F23861B87537200AE40CD /* Account.swift */; }; - ED0F23891B87543D00AE40CD /* DownloadQueueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F23881B87543D00AE40CD /* DownloadQueueObserver.swift */; }; + ED0F23891B87543D00AE40CD /* PurchaseDownloadObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F23881B87543D00AE40CD /* PurchaseDownloadObserver.swift */; }; ED0F238B1B87569C00AE40CD /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F238A1B87569C00AE40CD /* Downloader.swift */; }; ED0F238D1B8756E600AE40CD /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F238C1B8756E600AE40CD /* Error.swift */; }; ED0F23901B87A56F00AE40CD /* ISStoreAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F238F1B87A56F00AE40CD /* ISStoreAccount.swift */; }; + EDA3BE521B8B84AF00C18D70 /* SSPurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA3BE511B8B84AF00C18D70 /* SSPurchase.swift */; }; EDEAA0C01B51CE6200F2FC3F /* StoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDEAA0BF1B51CE6200F2FC3F /* StoreFoundation.framework */; }; EDEAA17D1B5C579100F2FC3F /* CommerceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDEAA17C1B5C579100F2FC3F /* CommerceKit.framework */; }; F5F01044EC3065C6EBAB95D7 /* BoxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E87CFA5E6371893D5B1807 /* BoxType.swift */; }; @@ -57,10 +58,11 @@ ED0F23821B87533A00AE40CD /* ListInstalled.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListInstalled.swift; sourceTree = ""; }; ED0F23841B87536A00AE40CD /* ListUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListUpdates.swift; sourceTree = ""; }; ED0F23861B87537200AE40CD /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; - ED0F23881B87543D00AE40CD /* DownloadQueueObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadQueueObserver.swift; sourceTree = ""; }; + ED0F23881B87543D00AE40CD /* PurchaseDownloadObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseDownloadObserver.swift; sourceTree = ""; }; ED0F238A1B87569C00AE40CD /* Downloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; ED0F238C1B8756E600AE40CD /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; ED0F238F1B87A56F00AE40CD /* ISStoreAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISStoreAccount.swift; sourceTree = ""; }; + EDA3BE511B8B84AF00C18D70 /* SSPurchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSPurchase.swift; sourceTree = ""; }; EDEAA0BF1B51CE6200F2FC3F /* StoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreFoundation.framework; path = /System/Library/PrivateFrameworks/StoreFoundation.framework; sourceTree = ""; }; EDEAA0C31B51CEE400F2FC3F /* CDStructures.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CDStructures.h; sourceTree = ""; }; EDEAA0C41B51CEE400F2FC3F /* CKBook.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKBook.h; sourceTree = ""; }; @@ -351,8 +353,9 @@ isa = PBXGroup; children = ( ED0F238A1B87569C00AE40CD /* Downloader.swift */, - ED0F23881B87543D00AE40CD /* DownloadQueueObserver.swift */, ED0F238F1B87A56F00AE40CD /* ISStoreAccount.swift */, + ED0F23881B87543D00AE40CD /* PurchaseDownloadObserver.swift */, + EDA3BE511B8B84AF00C18D70 /* SSPurchase.swift */, ); path = AppStore; sourceTree = ""; @@ -637,7 +640,6 @@ F5F01044EC3065C6EBAB95D7 /* BoxType.swift in Sources */, 21EC092422A3EDFE33B153B8 /* Command.swift in Sources */, ED0F238B1B87569C00AE40CD /* Downloader.swift in Sources */, - ED0F23891B87543D00AE40CD /* DownloadQueueObserver.swift in Sources */, ED0F238D1B8756E600AE40CD /* Error.swift in Sources */, B80C5DDD38A8F7EB6F320697 /* Errors.swift in Sources */, 5918483F96256CDAC88FF450 /* HelpCommand.swift in Sources */, @@ -648,7 +650,9 @@ ED031A7C1B5127C00097692E /* main.swift in Sources */, DE39BCA91D1BC3D876711677 /* MutableBox.swift in Sources */, 92828DCD99CED47F54242776 /* Option.swift in Sources */, + ED0F23891B87543D00AE40CD /* PurchaseDownloadObserver.swift in Sources */, 1CC607DA6B900AA3FEC3F6D8 /* Result.swift in Sources */, + EDA3BE521B8B84AF00C18D70 /* SSPurchase.swift in Sources */, 7858BCFB4D5A4251DE998CE4 /* Switch.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/mas-cli/AppStore/Downloader.swift b/mas-cli/AppStore/Downloader.swift index de4d808..1d48620 100644 --- a/mas-cli/AppStore/Downloader.swift +++ b/mas-cli/AppStore/Downloader.swift @@ -6,25 +6,34 @@ // Copyright (c) 2015 Andrew Naylor. All rights reserved. // -typealias DownloadCompletion = (purchase: SSPurchase!, completed: Bool, error: NSError?, response: SSPurchaseResponse!) -> () +func download(adamId: UInt64) -> MASError? { -func download(adamId: UInt64, completion:DownloadCompletion) { - let buyParameters = "productType=C&price=0&salableAdamId=\(adamId)&pricingParameters=STDRDL" - let purchase = SSPurchase() - purchase.buyParameters = buyParameters - purchase.itemIdentifier = adamId - purchase.accountIdentifier = primaryAccount().dsID - purchase.appleID = primaryAccount().identifier - - let downloadMetadata = SSDownloadMetadata() - downloadMetadata.kind = "software" - downloadMetadata.itemIdentifier = adamId - - purchase.downloadMetadata = downloadMetadata - - let purchaseController = CKPurchaseController.sharedPurchaseController() - purchaseController.performPurchase(purchase, withOptions: 0, completionHandler: completion) - while true { - NSRunLoop.mainRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 10)) + if let account = ISStoreAccount.primaryAccount { + let group = dispatch_group_create() + let purchase = SSPurchase(adamId: adamId, account: account) + + var purchaseError: MASError? + + purchase.perform { purchase, completed, error, response in + if completed { + let observer = PurchaseDownloadObserver(purchase: purchase) + observer.onCompletion { + dispatch_group_leave(group) + } + + CKDownloadQueue.sharedDownloadQueue().addObserver(observer) + } + else { + purchaseError = MASError(code: .PurchaseError, sourceError: error) + dispatch_group_leave(group) + } + } + + dispatch_group_enter(group) + dispatch_group_wait(group, DISPATCH_TIME_FOREVER) + return purchaseError } -} \ No newline at end of file + else { + return MASError(code: .NotSignedIn) + } +} diff --git a/mas-cli/AppStore/DownloadQueueObserver.swift b/mas-cli/AppStore/PurchaseDownloadObserver.swift similarity index 82% rename from mas-cli/AppStore/DownloadQueueObserver.swift rename to mas-cli/AppStore/PurchaseDownloadObserver.swift index 4fba148..a486be9 100644 --- a/mas-cli/AppStore/DownloadQueueObserver.swift +++ b/mas-cli/AppStore/PurchaseDownloadObserver.swift @@ -1,5 +1,5 @@ // -// DownloadQueueObserver.swift +// PurchaseDownloadObserver.swift // mas-cli // // Created by Andrew Naylor on 21/08/2015. @@ -8,9 +8,15 @@ let csi = "\u{001B}[" -@objc class DownloadQueueObserver: CKDownloadQueueObserver { +@objc class PurchaseDownloadObserver: CKDownloadQueueObserver { + let purchase: SSPurchase + var completionHandler: (() -> ())? var started = false + init(purchase: SSPurchase) { + self.purchase = purchase + } + func downloadQueue(queue: CKDownloadQueue, statusChangedForDownload download: SSDownload!) { if !started { return @@ -26,7 +32,13 @@ let csi = "\u{001B}[" func downloadQueue(queue: CKDownloadQueue, changedWithRemoval download: SSDownload!) { println("") println("==> Installed " + download.metadata.title) - exit(EXIT_SUCCESS) + if let complete = self.completionHandler { + complete() + } + } + + func onCompletion(complete: () -> ()) { + self.completionHandler = complete } } diff --git a/mas-cli/AppStore/SSPurchase.swift b/mas-cli/AppStore/SSPurchase.swift new file mode 100644 index 0000000..fe873b6 --- /dev/null +++ b/mas-cli/AppStore/SSPurchase.swift @@ -0,0 +1,29 @@ +// +// SSPurchase.swift +// mas-cli +// +// Created by Andrew Naylor on 25/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +typealias SSPurchaseCompletion = (purchase: SSPurchase!, completed: Bool, error: NSError?, response: SSPurchaseResponse!) -> () + +extension SSPurchase { + convenience init(adamId: UInt64, account: ISStoreAccount) { + self.init() + self.buyParameters = "productType=C&price=0&salableAdamId=\(adamId)&pricingParameters=STDRDL" + self.itemIdentifier = adamId + self.accountIdentifier = account.dsID + self.appleID = account.identifier + + let downloadMetadata = SSDownloadMetadata() + downloadMetadata.kind = "software" + downloadMetadata.itemIdentifier = adamId + + self.downloadMetadata = downloadMetadata + } + + func perform(completion: SSPurchaseCompletion) { + CKPurchaseController.sharedPurchaseController().performPurchase(self, withOptions: 0, completionHandler: completion) + } +} diff --git a/mas-cli/Commands/Install.swift b/mas-cli/Commands/Install.swift index 606830a..8221737 100644 --- a/mas-cli/Commands/Install.swift +++ b/mas-cli/Commands/Install.swift @@ -11,10 +11,18 @@ struct InstallCommand: CommandType { let function = "Install from the Mac App Store" func run(mode: CommandMode) -> Result<(), CommandantError> { - return InstallOptions.evaluate(mode).map { options in - download(options.appId) { (purchase, completed, error, response) in - + let optionsResult = InstallOptions.evaluate(mode) + + switch optionsResult { + case let .Failure(error): + return .Failure(error) + + case let .Success(options): + if let error = download(options.value.appId) { + return .failure(CommandantError.CommandError(Box(error))) } + + return .success(()) } } } diff --git a/mas-cli/Error.swift b/mas-cli/Error.swift index 129adc8..ff52e31 100644 --- a/mas-cli/Error.swift +++ b/mas-cli/Error.swift @@ -13,6 +13,7 @@ private let MASErrorSource: String = "MASErrorSource" public enum MASErrorCode: Int { case NoError case NotSignedIn + case PurchaseError var exitCode: Int32 { return Int32(self.rawValue) diff --git a/mas-cli/main.swift b/mas-cli/main.swift index bb4e425..0b57e4e 100644 --- a/mas-cli/main.swift +++ b/mas-cli/main.swift @@ -8,8 +8,6 @@ import Foundation -var downloadQueue = CKDownloadQueue.sharedDownloadQueue() -downloadQueue.addObserver(DownloadQueueObserver()) let registry = CommandRegistry() let helpCommand = HelpCommand(registry: registry)