Merge pull request #264 from blochberger/feature/purchase-apps

Add support to purchase apps
This commit is contained in:
Ben Chatelain 2020-05-14 20:15:07 -06:00 committed by GitHub
commit 04686cf85b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 139 additions and 9 deletions

View file

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- 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

View file

@ -13,14 +13,14 @@ import StoreFoundation
///
/// - Parameter adamId: An app ID?
/// - Returns: An error, if one occurred.
func download(_ adamId: UInt64) -> MASError? {
func download(_ adamId: UInt64, isPurchase: Bool) -> 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)
let purchase = SSPurchase(adamId: adamId, account: storeAccount, isPurchase: isPurchase)
var purchaseError: MASError?
var observerIdentifier: CKDownloadQueueObserver?

View file

@ -13,14 +13,39 @@ typealias SSPurchaseCompletion =
(_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void
extension SSPurchase {
convenience init(adamId: UInt64, account: ISStoreAccount) {
convenience init(adamId: UInt64, account: ISStoreAccount, isPurchase: Bool) {
self.init()
buyParameters =
"productType=C&price=0&salableAdamId=\(adamId)&pricingParameters=STDRDL&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"
} else {
// is redownload, use existing functionality
parameters["pricingParameters"] = "STDRDL"
}
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 {
isRedownload = false
}
let downloadMetadata = SSDownloadMetadata()
downloadMetadata.kind = "software"
downloadMetadata.itemIdentifier = adamId

View file

@ -38,7 +38,7 @@ public struct InstallCommand: CommandProtocol {
return nil
}
return download(appId)
return download(appId, isPurchase: false)
}
switch downloadResults.count {

View file

@ -73,7 +73,7 @@ public struct LuckyCommand: CommandProtocol {
return nil
}
return download(appId)
return download(appId, isPurchase: false)
}
switch downloadResults.count {

View file

@ -0,0 +1,62 @@
//
// Purchase.swift
// mas-cli
//
// Created by Jakob Rieck on 24/10/2017.
// Copyright (c) 2017 Jakob Rieck. All rights reserved.
//
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 struct PurchaseOptions: OptionsProtocol {
let appIds: [UInt64]
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")
}
}

View file

@ -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)
download($0.itemIdentifier.uint64Value, isPurchase: false)
}
switch updateResults.count {

View file

@ -0,0 +1,24 @@
//
// PurchaseCommandSpec.swift
// MasKitTests
//
// Created by Maximilian Blochberger on 2020-03-21.
// Copyright © 2020 mas-cli. All rights reserved.
//
@testable import MasKit
import Nimble
import Quick
class PurchaseCommandSpec: QuickSpec {
override func spec() {
describe("purchase command") {
it("purchases apps") {
let cmd = PurchaseCommand()
let result = cmd.run(PurchaseCommand.Options(appIds: []))
print(result)
// expect(result).to(beSuccess())
}
}
}
}

View file

@ -92,7 +92,16 @@ $ mas lucky twitter
```
> Please note that this command will not allow you to install (or even purchase) an app for the first time:
it must already be in the Purchased tab of the App Store.
use the `purchase` command in that case.
```bash
$ mas purchase 768053424
==> Downloading Gapplin
==> Installed Gapplin
```
> Please note that you may have to re-authenticate yourself in the App Store to complete the purchase.
This is the case if the application is not free or if you configured your account not to remember the credentials for free purchases.
Use `mas outdated` to list all applications with pending updates.

View file

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
60D8CF3624262F92005B4004 /* PurchaseCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60D8CF3524262F92005B4004 /* PurchaseCommandSpec.swift */; };
75FB3E761F9F7841005B6F20 /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75FB3E751F9F7841005B6F20 /* Purchase.swift */; };
B537017421A0F85B00538F78 /* Commandant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406B213F4DDD0044E445 /* Commandant.framework */; };
B537017621A0F94200538F78 /* Commandant.framework in Copy Carthage Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406B213F4DDD0044E445 /* Commandant.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B5552928219A1BB900ACB4CA /* CommerceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F83213A62173EF75008BA8A0 /* CommerceKit.framework */; };
@ -195,7 +197,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
60D8CF3524262F92005B4004 /* PurchaseCommandSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCommandSpec.swift; sourceTree = "<group>"; };
693A98981CBFFA760004D3B4 /* Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
75FB3E751F9F7841005B6F20 /* Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = "<group>"; };
8078FAA71EC4F2FB004B5B3F /* Lucky.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lucky.swift; sourceTree = "<group>"; };
900A1E801DBAC8CB0069B1A8 /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = "<group>"; };
90CB4069213F4DDD0044E445 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = "<group>"; };
@ -606,6 +610,7 @@
B594B14321D6D91800F3AC59 /* LuckyCommandSpec.swift */,
B5DBF81221DEEC7C00F3B151 /* OpenCommandSpec.swift */,
B594B14121D6D8EC00F3AC59 /* OutdatedCommandSpec.swift */,
60D8CF3524262F92005B4004 /* PurchaseCommandSpec.swift */,
B594B13F21D6D8BF00F3AC59 /* ResetCommandSpec.swift */,
B594B13D21D6D78900F3AC59 /* SearchCommandSpec.swift */,
B594B13B21D6D72E00F3AC59 /* SignInCommandSpec.swift */,
@ -670,6 +675,7 @@
8078FAA71EC4F2FB004B5B3F /* Lucky.swift */,
B5DBF80C21DEE4E600F3B151 /* Open.swift */,
ED0F23841B87536A00AE40CD /* Outdated.swift */,
75FB3E751F9F7841005B6F20 /* Purchase.swift */,
EDCBF9521D89AC6F000039C6 /* Reset.swift */,
693A98981CBFFA760004D3B4 /* Search.swift */,
EDC90B641C70045E0019E396 /* SignIn.swift */,
@ -1044,6 +1050,7 @@
F8FB717D20F2B4DD00F56FDC /* Utilities.swift in Sources */,
B5DBF80F21DEEB7B00F3B151 /* Vendor.swift in Sources */,
F8FB717A20F2B4DD00F56FDC /* Version.swift in Sources */,
75FB3E761F9F7841005B6F20 /* Purchase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1073,6 +1080,7 @@
B576FE3021E5BD130016B39D /* OutputListenerSpec.swift in Sources */,
B594B14021D6D8BF00F3AC59 /* ResetCommandSpec.swift in Sources */,
B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */,
60D8CF3624262F92005B4004 /* PurchaseCommandSpec.swift in Sources */,
B594B13E21D6D78900F3AC59 /* SearchCommandSpec.swift in Sources */,
B55B3D9221ED9B8C0009A1A5 /* SearchResultFormatterSpec.swift in Sources */,
B594B13C21D6D72E00F3AC59 /* SignInCommandSpec.swift in Sources */,

View file

@ -23,6 +23,7 @@ registry.register(AccountCommand())
registry.register(HomeCommand())
registry.register(InfoCommand())
registry.register(InstallCommand())
registry.register(PurchaseCommand())
registry.register(ListCommand())
registry.register(LuckyCommand())
registry.register(OpenCommand())