Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/OversizeAppStoreServices/Auth/EnvAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import AppStoreConnect
import Factory
import Foundation
import OversizeCore
import OversizeServices

public struct EnvAuthenticator: Authenticator {
Expand All @@ -18,14 +19,17 @@ public struct EnvAuthenticator: Authenticator {

) throws {
guard let keyLabel = UserDefaults.standard.string(forKey: "AppStore.Account") else {
logError("Get UserDefaults value for 'AppStore.Account'")
throw Error.missingEnvironmentVariable("AppStore.Account")
}

guard let appStoreIssuerID = storage.getPassword(for: "AppConnector-IssuerID-" + keyLabel) else {
logError("Get Keychain value for IssuerID")
throw Error.missingEnvironmentVariable("AppStore.Key.Default")
}

guard let appStoreCertificate = storage.getCredentials(with: "AppConnector-Certificate-" + keyLabel) else {
logError("Get Keychain value for Certificate")
throw Error.missingEnvironmentVariable("AppStore.Key.Default")
}

Expand Down
12 changes: 11 additions & 1 deletion Sources/OversizeAppStoreServices/Cache/СacheService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ public actor CacheService {
}

public func clearAll() async {
try? FileManager.default.removeItem(at: cacheDirectory)
do {
if FileManager.default.fileExists(atPath: cacheDirectory.path) {
try FileManager.default.removeItem(at: cacheDirectory)
}

try FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)

logData("Cache cleared successfully")
} catch {
logError("Failed to clear cache: \(error)")
}
}

private func isCacheValid(for fileURL: URL) async -> Bool {
Expand Down
24 changes: 24 additions & 0 deletions Sources/OversizeAppStoreServices/Models/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public struct App: Identifiable, Sendable {

public var included: Included?

public init?(response: AppStoreAPI.AppResponse) {
self.init(schema: response.data, included: response.included)
}

public init?(schema: AppStoreAPI.App) {
guard let attributes = schema.attributes,
let bundleID = attributes.bundleID,
Expand Down Expand Up @@ -212,3 +216,23 @@ public extension App {
name.components(separatedBy: [".", "-", "—"]).last ?? ""
}
}

extension App {
static func from(response: AppStoreAPI.AppsResponse) -> [App] {
response.data.compactMap { schema in
let filteredIncluded = response.included?.filter { includedItem in
switch includedItem {
case let .appStoreVersion(appStoreVersion):
schema.relationships?.appStoreVersions?.data?.contains(where: { $0.id == appStoreVersion.id }) ?? false
case let .build(build):
schema.relationships?.builds?.data?.contains(where: { $0.id == build.id }) ?? false
case let .prereleaseVersion(prereleaseVersion):
schema.relationships?.preReleaseVersions?.data?.contains(where: { $0.id == prereleaseVersion.id }) ?? false
default:
false
}
}
return App(schema: schema, included: filteredIncluded)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ public struct SubscriptionLocalization: Codable, Equatable, Identifiable, Sendab
// Computed property for status color
public var statusColor: Color {
switch self {
case .prepareForSubmission: .gray
case .waitingForReview: .yellow
case .waitingForReview, .prepareForSubmission: .yellow
case .approved: .green
case .rejected: .red
}
Expand Down
148 changes: 40 additions & 108 deletions Sources/OversizeAppStoreServices/Services/AppsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ public actor AppsService {
guard let client else { return .failure(.network(type: .unauthorized)) }
return await cacheService.fetchWithCache(key: "fetchApp\(id)", force: force) {
let request = Resources.v1.apps.id(id).get()
let response = try await client.send(request)
return response.data
}.flatMap { data in
guard let app = App(schema: data) else {
return try await client.send(request)
}.flatMap {
guard let app = App(response: $0) else {
return .failure(.network(type: .decode))
}
return .success(app)
Expand All @@ -38,7 +37,7 @@ public actor AppsService {

public func fetchAppIncludeBuildsAndAppStoreVersions(id: String, force: Bool = false) async -> Result<App, AppError> {
guard let client else { return .failure(.network(type: .unauthorized)) }
return await cacheService.fetchWithCache(key: "fetchApp\(id)", force: force) {
return await cacheService.fetchWithCache(key: "fetchAppIncludeBuildsAndAppStoreVersions\(id)", force: force) {
let request = Resources.v1.apps.id(id).get(
include: [
.builds,
Expand All @@ -47,7 +46,7 @@ public actor AppsService {
)
return try await client.send(request)
}.flatMap {
guard let app = App(schema: $0.data, included: $0.included) else {
guard let app = App(response: $0) else {
return .failure(.network(type: .decode))
}
return .success(app)
Expand Down Expand Up @@ -80,39 +79,8 @@ public actor AppsService {
guard let client else { return .failure(.network(type: .unauthorized)) }
let request = Resources.v1.apps.get()
do {
let data = try await client.send(request).data
let apps: [App] = data.compactMap { .init(schema: $0) }
return .success(apps)
} catch {
return .failure(.network(type: .noResponse))
}
}

public func fetchAppsIncludeAppStoreVersionsAndPreReleaseVersions() async -> Result<[App], AppError> {
guard let client else {
return .failure(.network(type: .unauthorized))
}
let request = Resources.v1.apps.get(
include: [
.appStoreVersions,
.preReleaseVersions,
]
)
do {
let result = try await client.send(request)
let apps: [App] = result.data.compactMap { schema in
let filteredIncluded = result.included?.filter { includedItem in
switch includedItem {
case let .appStoreVersion(appStoreVersion):
schema.relationships?.appStoreVersions?.data?.first(where: { $0.id == appStoreVersion.id }) != nil
case let .prereleaseVersion(prereleaseVersion):
schema.relationships?.preReleaseVersions?.data?.first(where: { $0.id == prereleaseVersion.id }) != nil
default:
false
}
}
return App(schema: schema, included: filteredIncluded)
}
let response = try await client.send(request)
let apps: [App] = App.from(response: response)
return .success(apps)
} catch {
return .failure(.network(type: .noResponse))
Expand All @@ -130,60 +98,42 @@ public actor AppsService {
]
)
return try await client.send(request)
}.flatMap {
.success(processAppsResponse($0))
}
}.map { App.from(response: $0) }
}

public func fetchAppsIncludeActualAppStoreVersionsAndBuilds(limitAppStoreVersions: Int? = nil) async -> Result<[App], AppError> {
public func fetchAppsIncludeActualAppStoreVersionsAndBuilds(limitAppStoreVersions: Int? = nil, forse: Bool = false) async -> Result<[App], AppError> {
guard let client else { return .failure(.network(type: .unauthorized)) }
let request = Resources.v1.apps.get(
filterAppStoreVersionsAppStoreState: [
.accepted,
.developerRemovedFromSale,
.developerRejected,
.inReview,
.invalidBinary,
.metadataRejected,
.pendingAppleRelease,
.pendingContract,
.pendingDeveloperRelease,
.prepareForSubmission,
.preorderReadyForSale,
.processingForAppStore,
.readyForReview,
.readyForSale,
.rejected,
.removedFromSale,
.waitingForExportCompliance,
.waitingForReview,
.notApplicable,
],
include: [
.builds,
.appStoreVersions,
],
limitAppStoreVersions: limitAppStoreVersions
)
do {
let result = try await client.send(request)
let apps: [App] = result.data.compactMap { schema in
let filteredIncluded = result.included?.filter { includedItem in
switch includedItem {
case let .appStoreVersion(appStoreVersion):
schema.relationships?.appStoreVersions?.data?.first(where: { $0.id == appStoreVersion.id }) != nil
case let .build(build):
schema.relationships?.builds?.data?.first(where: { $0.id == build.id }) != nil
default:
false
}
}
return App(schema: schema, included: filteredIncluded)
}
return .success(apps)
} catch {
return .failure(.network(type: .noResponse))
}
return await cacheService.fetchWithCache(key: "fetchAppsIncludeActualAppStoreVersionsAndBuilds", force: forse) {
let request = Resources.v1.apps.get(
filterAppStoreVersionsAppStoreState: [
.accepted,
.developerRemovedFromSale,
.developerRejected,
.inReview,
.invalidBinary,
.metadataRejected,
.pendingAppleRelease,
.pendingContract,
.pendingDeveloperRelease,
.prepareForSubmission,
.preorderReadyForSale,
.processingForAppStore,
.readyForReview,
.readyForSale,
.rejected,
.removedFromSale,
.waitingForExportCompliance,
.waitingForReview,
.notApplicable,
],
include: [
.builds,
.appStoreVersions,
],
limitAppStoreVersions: limitAppStoreVersions
)
return try await client.send(request)
}.map { App.from(response: $0) }
}

func postBundleId(
Expand Down Expand Up @@ -240,24 +190,6 @@ public actor AppsService {
}

private extension AppsService {
func processAppsResponse(_ response: AppsResponse) -> [App] {
response.data.compactMap { schema in
let filteredIncluded = response.included?.filter { includedItem in
switch includedItem {
case let .appStoreVersion(appStoreVersion):
schema.relationships?.appStoreVersions?.data?.contains(where: { $0.id == appStoreVersion.id }) ?? false
case let .build(build):
schema.relationships?.builds?.data?.contains(where: { $0.id == build.id }) ?? false
case let .prereleaseVersion(prereleaseVersion):
schema.relationships?.preReleaseVersions?.data?.contains(where: { $0.id == prereleaseVersion.id }) ?? false
default:
false
}
}
return App(schema: schema, included: filteredIncluded)
}
}

func handleRequestFailure<T>(error: Error, replaces: [String: String] = [:]) -> Result<T, AppError> {
if let responseError = error as? ResponseError {
switch responseError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,17 @@ public actor SubscriptionsService {
return handleRequestFailure(error: error, replaces: [:])
}
}

public func deleteSubscription(subscriptionsId: String) async -> Result<Bool, AppError> {
guard let client else { return .failure(.network(type: .unauthorized)) }
let request = Resources.v1.subscriptions.id(subscriptionsId).delete
do {
let _ = try await client.send(request)
return .success(true)
} catch {
return .failure(.network(type: .noResponse))
}
}
}

extension SubscriptionsService {
Expand Down
19 changes: 12 additions & 7 deletions Sources/OversizeAppStoreServices/Services/VersionsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ public actor VersionsService {
}
}

public func fetchAppVersions(appId: String) async -> Result<[AppStoreVersion], AppError> {
public func fetchAppVersions(appId: String, platform: Platform? = nil, force: Bool = false) async -> Result<[AppStoreVersion], AppError> {
guard let client else { return .failure(.network(type: .unauthorized)) }
let request = Resources.v1.apps.id(appId).appStoreVersions.get()
do {
let data = try await client.send(request).data
return .success(data.compactMap { .init(schema: $0) })
} catch {
return .failure(.network(type: .noResponse))
let filterPlatforms: [Resources.V1.Apps.WithID.AppStoreVersions.FilterPlatform]? = if let platform, let filteredPlatform: Resources.V1.Apps.WithID.AppStoreVersions.FilterPlatform = .init(rawValue: platform.rawValue) {
[filteredPlatform]
} else {
nil
}
return await cacheService.fetchWithCache(key: "fetchAppVersions\(appId)\(platform?.rawValue ?? "")", force: force) {
let request = Resources.v1.apps.id(appId).appStoreVersions.get(filterPlatform: filterPlatforms)
let response = try await client.send(request)
return response.data
}.map { data in
data.compactMap { .init(schema: $0) }
}
}

Expand Down