mirror of
https://github.com/mas-cli/mas
synced 2025-02-16 12:38:30 +00:00
Merge pull request #264 from blochberger/feature/purchase-apps
Add support to purchase apps
This commit is contained in:
commit
04686cf85b
11 changed files with 139 additions and 9 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -38,7 +38,7 @@ public struct InstallCommand: CommandProtocol {
|
|||
return nil
|
||||
}
|
||||
|
||||
return download(appId)
|
||||
return download(appId, isPurchase: false)
|
||||
}
|
||||
|
||||
switch downloadResults.count {
|
||||
|
|
|
@ -73,7 +73,7 @@ public struct LuckyCommand: CommandProtocol {
|
|||
return nil
|
||||
}
|
||||
|
||||
return download(appId)
|
||||
return download(appId, isPurchase: false)
|
||||
}
|
||||
|
||||
switch downloadResults.count {
|
||||
|
|
62
MasKit/Commands/Purchase.swift
Normal file
62
MasKit/Commands/Purchase.swift
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
24
MasKitTests/Commands/PurchaseCommandSpec.swift
Normal file
24
MasKitTests/Commands/PurchaseCommandSpec.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
README.md
11
README.md
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Add table
Reference in a new issue