Skip to content

Commit

Permalink
Merge pull request #272 from mas-cli/purchase-cleanup
Browse files Browse the repository at this point in the history
🧹 Purchase cleanup
  • Loading branch information
phatblat committed May 15, 2020
2 parents 04686cf + 8516d96 commit 38f20a8
Show file tree
Hide file tree
Showing 39 changed files with 147 additions and 142 deletions.
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@
excluded:
- Carthage
- docs
- MasKitTests
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

- ✨ New `purchase` command for purchasing free apps #264 (resolves #2, #145)
thanks, [@blochberger](https://github.com/blochberger)!
- 🐟 Seriously more interactive fish completions #242
thanks, [@lwolfsonkin](https://github.com/lwolfsonkin)!
- 🧹 Purchase cleanup #272
- ♻️ SoftwareMap Protocol #271
- 🕊 Swift 5 #255
- ⚒️ Xcode 10.2 and macOS 10.14 required to build
- ⬆️ Commandant (0.17.0) #255
- ⬆️ Nimble (8.0.4) #255
- ⬆️ Quick (2.2.0) #255
- ➖ Result #255
- Seriously more interactive fish completions #242
thanks, [@lwolfsonkin](https://github.com/lwolfsonkin)!
- 💡 Update readme with simpler tap usage #241
- Added support for purchasing apps (#2, #145)

## [v1.6.4] 🔎 Search Fix - 2020-05-11

Expand Down
8 changes: 5 additions & 3 deletions MasKit/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ import StoreFoundation
/// Monitors app download progress.
///
/// - Parameter adamId: An app ID?
/// - Parameter purchase: Flag indicating whether the app needs to be purchased.
/// Only works for free apps. Defaults to false.
/// - Returns: An error, if one occurred.
func download(_ adamId: UInt64, isPurchase: Bool) -> MASError? {
func download(_ adamId: UInt64, purchase: Bool = false) -> MASError? {
guard let account = ISStoreAccount.primaryAccount else {
return .notSignedIn
}

guard let storeAccount = account as? ISStoreAccount
else { fatalError("Unable to cast StoreAccount to ISStoreAccount") }
let purchase = SSPurchase(adamId: adamId, account: storeAccount, isPurchase: isPurchase)
else { fatalError("Unable to cast StoreAccount to ISStoreAccount") }
let purchase = SSPurchase(adamId: adamId, account: storeAccount, purchase: purchase)

var purchaseError: MASError?
var observerIdentifier: CKDownloadQueueObserver?
Expand Down
4 changes: 2 additions & 2 deletions MasKit/AppStore/PurchaseDownloadObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import StoreFoundation
func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
let status = download.status else {
return
return
}

if status.isFailed || status.isCancelled {
Expand All @@ -42,7 +42,7 @@ import StoreFoundation
func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
let status = download.status else {
return
return
}

clearLine()
Expand Down
32 changes: 16 additions & 16 deletions MasKit/AppStore/SSPurchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,36 @@ typealias SSPurchaseCompletion =
(_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void

extension SSPurchase {
convenience init(adamId: UInt64, account: ISStoreAccount, isPurchase: Bool) {
convenience init(adamId: UInt64, account: ISStoreAccount, purchase: Bool = false) {
self.init()

var parameters: [String: Any] = [
"productType": "C",
"price": 0,
"salableAdamId": adamId,
"pg": "default",
"appExtVrsId": 0
]
var parameters: [String: Any] = [
"productType": "C",
"price": 0,
"salableAdamId": adamId,
"pg": "default",
"appExtVrsId": 0
]

if isPurchase {
parameters["macappinstalledconfirmed"] = 1
parameters["pricingParameters"] = "STDQ"
if purchase {
parameters["macappinstalledconfirmed"] = 1
parameters["pricingParameters"] = "STDQ"

} else {
// is redownload, use existing functionality
parameters["pricingParameters"] = "STDRDL"
parameters["pricingParameters"] = "STDRDL"
}

buyParameters = parameters.map { key, value in
return "\(key)=\(value)"
}.joined(separator: "&")
buyParameters = parameters.map { key, value in
return "\(key)=\(value)"
}.joined(separator: "&")

itemIdentifier = adamId
accountIdentifier = account.dsID
appleID = account.identifier

// Not sure if this is needed, but lets use it here.
if isPurchase {
if purchase {
isRedownload = false
}

Expand Down
3 changes: 1 addition & 2 deletions MasKit/Commands/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ public struct HomeCommand: CommandProtocol {
/// Runs the command.
public func run(_ options: HomeOptions) -> Result<(), MASError> {
do {
guard let result = try storeSearch.lookup(app: options.appId)
else {
guard let result = try storeSearch.lookup(app: options.appId) else {
print("No results found")
return .failure(.noSearchResultsFound)
}
Expand Down
3 changes: 1 addition & 2 deletions MasKit/Commands/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ public struct InfoCommand: CommandProtocol {
/// Runs the command.
public func run(_ options: InfoOptions) -> Result<(), MASError> {
do {
guard let result = try storeSearch.lookup(app: options.appId)
else {
guard let result = try storeSearch.lookup(app: options.appId) else {
print("No results found")
return .failure(.noSearchResultsFound)
}
Expand Down
3 changes: 1 addition & 2 deletions MasKit/Commands/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public struct InstallCommand: CommandProtocol {
private let appLibrary: AppLibrary

/// Public initializer.
/// - Parameter appLibrary: AppLibrary manager.
public init() {
self.init(appLibrary: MasAppLibrary())
}
Expand All @@ -38,7 +37,7 @@ public struct InstallCommand: CommandProtocol {
return nil
}

return download(appId, isPurchase: false)
return download(appId)
}

switch downloadResults.count {
Expand Down
6 changes: 3 additions & 3 deletions MasKit/Commands/Lucky.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public struct LuckyCommand: CommandProtocol {
/// - Parameter storeSearch: Search manager.
init(appLibrary: AppLibrary = MasAppLibrary(),
storeSearch: StoreSearch = MasStoreSearch()) {
self.appLibrary = appLibrary
self.storeSearch = storeSearch
self.appLibrary = appLibrary
self.storeSearch = storeSearch
}

/// Runs the command.
Expand Down Expand Up @@ -73,7 +73,7 @@ public struct LuckyCommand: CommandProtocol {
return nil
}

return download(appId, isPurchase: false)
return download(appId)
}

switch downloadResults.count {
Expand Down
16 changes: 8 additions & 8 deletions MasKit/Commands/Open.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,20 @@ public struct OpenCommand: CommandProtocol {
}

guard let appId = Int(options.appId)
else {
print("Invalid app ID")
return .failure(.noSearchResultsFound)
else {
print("Invalid app ID")
return .failure(.noSearchResultsFound)
}

guard let result = try storeSearch.lookup(app: appId)
else {
print("No results found")
return .failure(.noSearchResultsFound)
else {
print("No results found")
return .failure(.noSearchResultsFound)
}

guard var url = URLComponents(string: result.trackViewUrl)
else {
return .failure(.searchFailed)
else {
return .failure(.searchFailed)
}
url.scheme = masScheme

Expand Down
4 changes: 2 additions & 2 deletions MasKit/Commands/Outdated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public struct OutdatedCommand: CommandProtocol {
if let installed = appLibrary.installedApp(forBundleId: update.bundleID) {
// Display version of installed app compared to available update.
print("""
\(update.itemIdentifier) \(update.title) (\(installed.bundleVersion) -> \(update.bundleVersion))
""")
\(update.itemIdentifier) \(update.title) (\(installed.bundleVersion) -> \(update.bundleVersion))
""")
} else {
print("\(update.itemIdentifier) \(update.title) (unknown -> \(update.bundleVersion))")
}
Expand Down
90 changes: 46 additions & 44 deletions MasKit/Commands/Purchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,55 @@ import Commandant
import CommerceKit

public struct PurchaseCommand: CommandProtocol {
public typealias Options = PurchaseOptions
public let verb = "purchase"
public let function = "Purchase and download free apps from the Mac App Store"

/// Designated initializer.
public init() {
}

/// Runs the command.
public func run(_ options: Options) -> Result<(), MASError> {
// Try to download applications with given identifiers and collect results
let downloadResults = options.appIds.compactMap { (appId) -> MASError? in
if let product = installedApp(appId) {
printWarning("\(product.appName) has already been purchased.")
return nil
}

return download(appId, isPurchase: true)
}

switch downloadResults.count {
case 0:
return .success(())
case 1:
return .failure(downloadResults[0])
default:
return .failure(.downloadFailed(error: nil))
}
}

fileprivate func installedApp(_ appId: UInt64) -> CKSoftwareProduct? {
let appId = NSNumber(value: appId)

let softwareMap = CKSoftwareMap.shared()
return softwareMap.allProducts()?.first { $0.itemIdentifier == appId }
}
public typealias Options = PurchaseOptions
public let verb = "purchase"
public let function = "Purchase and download free apps from the Mac App Store"

private let appLibrary: AppLibrary

/// Public initializer.
public init() {
self.init(appLibrary: MasAppLibrary())
}

/// Internal initializer.
/// - Parameter appLibrary: AppLibrary manager.
init(appLibrary: AppLibrary = MasAppLibrary()) {
self.appLibrary = appLibrary
}

/// Runs the command.
public func run(_ options: Options) -> Result<(), MASError> {
// Try to download applications with given identifiers and collect results
let downloadResults = options.appIds.compactMap { (appId) -> MASError? in
if let product = appLibrary.installedApp(forId: appId) {
printWarning("\(product.appName) has already been purchased.")
return nil
}

return download(appId, purchase: true)
}

switch downloadResults.count {
case 0:
return .success(())
case 1:
return .failure(downloadResults[0])
default:
return .failure(.downloadFailed(error: nil))
}
}
}

public struct PurchaseOptions: OptionsProtocol {
let appIds: [UInt64]
let appIds: [UInt64]

public static func create(_ appIds: [Int]) -> PurchaseOptions {
return PurchaseOptions(appIds: appIds.map { UInt64($0) })
}
public static func create(_ appIds: [Int]) -> PurchaseOptions {
return PurchaseOptions(appIds: appIds.map { UInt64($0) })
}

public static func evaluate(_ mode: CommandMode) -> Result<PurchaseOptions, CommandantError<MASError>> {
return create
<*> mode <| Argument(usage: "app ID(s) to install")
}
public static func evaluate(_ mode: CommandMode) -> Result<PurchaseOptions, CommandantError<MASError>> {
return create
<*> mode <| Argument(usage: "app ID(s) to install")
}
}
2 changes: 1 addition & 1 deletion MasKit/Commands/SignIn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public struct SignInOptions: OptionsProtocol {
static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions {
return { password in { dialog in
SignInOptions(username: username, password: password, dialog: dialog)
} }
} }
}

public static func evaluate(_ mode: CommandMode) -> Result<SignInOptions, CommandantError<MASError>> {
Expand Down
2 changes: 1 addition & 1 deletion MasKit/Commands/Upgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public struct UpgradeCommand: CommandProtocol {
print(updates.map({ "\($0.title) (\($0.bundleVersion))" }).joined(separator: ", "))

let updateResults = updates.compactMap {
download($0.itemIdentifier.uint64Value, isPurchase: false)
download($0.itemIdentifier.uint64Value)
}

switch updateResults.count {
Expand Down
8 changes: 4 additions & 4 deletions MasKit/Commands/Vendor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ public struct VendorCommand: CommandProtocol {
public func run(_ options: VendorOptions) -> Result<(), MASError> {
do {
guard let result = try storeSearch.lookup(app: options.appId)
else {
print("No results found")
return .failure(.noSearchResultsFound)
else {
print("No results found")
return .failure(.noSearchResultsFound)
}

guard let vendorWebsite = result.sellerUrl
else { throw MASError.noVendorWebsite }
else { throw MASError.noVendorWebsite }

do {
try openCommand.run(arguments: vendorWebsite)
Expand Down
Loading

0 comments on commit 38f20a8

Please sign in to comment.