From 1f9db7f684659945da2006f36510a56f1279710f Mon Sep 17 00:00:00 2001 From: hubab1 <50897577+hubab1@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:03:25 +0100 Subject: [PATCH] Support App Store URLs when adding apps --- OpenASO.xcodeproj/project.pbxproj | 8 +++ .../Features/AppManagement/AddAppSheet.swift | 22 ++++---- .../AppManagement/AppStoreIDInputParser.swift | 40 ++++++++++++++ OpenASOTests/AppStoreIDInputParserTests.swift | 54 +++++++++++++++++++ 4 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 OpenASO/Features/AppManagement/AppStoreIDInputParser.swift create mode 100644 OpenASOTests/AppStoreIDInputParserTests.swift diff --git a/OpenASO.xcodeproj/project.pbxproj b/OpenASO.xcodeproj/project.pbxproj index b90f3a0..9d3f741 100644 --- a/OpenASO.xcodeproj/project.pbxproj +++ b/OpenASO.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + F1A200000000000000000001 /* AppStoreIDInputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A200000000000000000002 /* AppStoreIDInputParser.swift */; }; + F1A200000000000000000003 /* AppStoreIDInputParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A200000000000000000004 /* AppStoreIDInputParserTests.swift */; }; B9A0000000000000000005 /* AppleAdsPastedSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A0000000000000000006 /* AppleAdsPastedSessionTests.swift */; }; B9A0000000000000000003 /* AppleAds/AppleAdsPastedSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A0000000000000000004 /* AppleAds/AppleAdsPastedSession.swift */; }; B9A0000000000000000001 /* AppleAds/AppleAdsWebLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A0000000000000000002 /* AppleAds/AppleAdsWebLoginController.swift */; }; @@ -326,6 +328,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + F1A200000000000000000002 /* AppStoreIDInputParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreIDInputParser.swift; sourceTree = ""; }; + F1A200000000000000000004 /* AppStoreIDInputParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreIDInputParserTests.swift; sourceTree = ""; }; B9A0000000000000000006 /* AppleAdsPastedSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAdsPastedSessionTests.swift; sourceTree = ""; }; B9A0000000000000000004 /* AppleAds/AppleAdsPastedSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAds/AppleAdsPastedSession.swift; sourceTree = ""; }; B9A0000000000000000002 /* AppleAds/AppleAdsWebLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAds/AppleAdsWebLoginController.swift; sourceTree = ""; }; @@ -615,6 +619,7 @@ 1027BC248FF4568E8523C3A8 /* OpenASOTests */ = { isa = PBXGroup; children = ( + F1A200000000000000000004 /* AppStoreIDInputParserTests.swift */, AA0000000000000000000001 /* AppleAdsConnectionStateTests.swift */, B9A0000000000000000006 /* AppleAdsPastedSessionTests.swift */, A60000000000000000000002 /* AIServiceTests.swift */, @@ -923,6 +928,7 @@ isa = PBXGroup; children = ( BCCBBF8A3FABA145BF5122ED /* AddAppSheet.swift */, + F1A200000000000000000002 /* AppStoreIDInputParser.swift */, ); path = AppManagement; sourceTree = ""; @@ -1169,6 +1175,7 @@ buildActionMask = 2147483647; files = ( EF3B2BA43807572789A250A7 /* AddAppSheet.swift in Sources */, + F1A200000000000000000001 /* AppStoreIDInputParser.swift in Sources */, 59E573ACDBBB76A905B92574 /* AddKeywordsSheet.swift in Sources */, B70000000000000000000001 /* AddKeywordsStorefrontProjection.swift in Sources */, B71000000000000000000001 /* AddKeywordsQueryResolver.swift in Sources */, @@ -1359,6 +1366,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F1A200000000000000000003 /* AppStoreIDInputParserTests.swift in Sources */, AA0000000000000000000002 /* AppleAdsConnectionStateTests.swift in Sources */, B9A0000000000000000005 /* AppleAdsPastedSessionTests.swift in Sources */, A60000000000000000000001 /* AIServiceTests.swift in Sources */, diff --git a/OpenASO/Features/AppManagement/AddAppSheet.swift b/OpenASO/Features/AppManagement/AddAppSheet.swift index b4c23a8..a78c663 100644 --- a/OpenASO/Features/AppManagement/AddAppSheet.swift +++ b/OpenASO/Features/AppManagement/AddAppSheet.swift @@ -59,22 +59,20 @@ struct AddAppSheet: View { } HStack { - TextField("Direct App Store ID", text: $appStoreIDText) + TextField("App Store ID or URL", text: $appStoreIDText) .textFieldStyle(.roundedBorder) .controlSize(.large) .font(.body) .onSubmit { - addByAppStoreID() + addDirectly() } - Button("Add by ID") { - addByAppStoreID() - } - .controlSize(.large) - .disabled(appStoreIDText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSearching) + Button("Add Directly", action: addDirectly) + .controlSize(.large) + .disabled(appStoreIDText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isSearching) } - Text("Search by name to pick the exact app or paste the numeric App Store ID for a direct lookup.") + Text("Search by name to pick the exact app, or paste its App Store URL or numeric ID for a direct lookup.") .font(.footnote) .foregroundStyle(.secondary) } @@ -161,17 +159,17 @@ struct AddAppSheet: View { } } - private func addByAppStoreID() { + private func addDirectly() { guard !isSearching else { return } - let trimmedAppStoreID = appStoreIDText.trimmingCharacters(in: .whitespacesAndNewlines) - guard let appStoreID = Int64(trimmedAppStoreID) else { - errorMessage = OpenASOError.invalidAppStoreID.localizedDescription + guard let appStoreID = AppStoreIDInputParser.appStoreID(from: appStoreIDText) else { + errorMessage = "Enter a valid numeric App Store ID or apps.apple.com URL." return } + appStoreIDText = String(appStoreID) isSearching = true errorMessage = nil diff --git a/OpenASO/Features/AppManagement/AppStoreIDInputParser.swift b/OpenASO/Features/AppManagement/AppStoreIDInputParser.swift new file mode 100644 index 0000000..4ff999d --- /dev/null +++ b/OpenASO/Features/AppManagement/AppStoreIDInputParser.swift @@ -0,0 +1,40 @@ +import Foundation + +enum AppStoreIDInputParser { + static func appStoreID(from input: String) -> Int64? { + let trimmedInput = input.trimmingCharacters(in: .whitespacesAndNewlines) + if let appStoreID = positiveAppStoreID(from: trimmedInput) { + return appStoreID + } + + guard let components = URLComponents(string: trimmedInput), + components.scheme?.lowercased() == "https", + components.host?.lowercased() == "apps.apple.com" + else { + return nil + } + + for pathComponent in components.path.split(separator: "/").reversed() { + guard pathComponent.hasPrefix("id") else { continue } + + let appStoreIDText = String(pathComponent.dropFirst(2)) + if let appStoreID = positiveAppStoreID(from: appStoreIDText) { + return appStoreID + } + } + + return nil + } + + private static func positiveAppStoreID(from text: String) -> Int64? { + guard !text.isEmpty, + text.allSatisfy(\.isNumber), + let appStoreID = Int64(text), + appStoreID > 0 + else { + return nil + } + + return appStoreID + } +} diff --git a/OpenASOTests/AppStoreIDInputParserTests.swift b/OpenASOTests/AppStoreIDInputParserTests.swift new file mode 100644 index 0000000..e0f3953 --- /dev/null +++ b/OpenASOTests/AppStoreIDInputParserTests.swift @@ -0,0 +1,54 @@ +import Testing +@testable import OpenASO + +struct AppStoreIDInputParserTests { + @Test + func acceptsNumericAppStoreID() { + #expect(AppStoreIDInputParser.appStoreID(from: "6793099062") == 6_793_099_062) + #expect(AppStoreIDInputParser.appStoreID(from: " 6793099062\n") == 6_793_099_062) + } + + @Test + func extractsAppStoreIDFromMacAppURL() { + let input = "https://apps.apple.com/in/app/viewio-app/id6793099062?mt=12" + + #expect(AppStoreIDInputParser.appStoreID(from: input) == 6_793_099_062) + } + + @Test + func extractsAppStoreIDFromSupportedAppStoreURLShapes() { + #expect( + AppStoreIDInputParser.appStoreID( + from: "https://apps.apple.com/us/app/example/id1234567890/" + ) == 1_234_567_890 + ) + #expect( + AppStoreIDInputParser.appStoreID( + from: "https://apps.apple.com/app/id1234567890#details" + ) == 1_234_567_890 + ) + } + + @Test + func rejectsInvalidOrUntrustedInput() { + #expect(AppStoreIDInputParser.appStoreID(from: "") == nil) + #expect(AppStoreIDInputParser.appStoreID(from: "0") == nil) + #expect(AppStoreIDInputParser.appStoreID(from: "-1") == nil) + #expect(AppStoreIDInputParser.appStoreID(from: "viewio") == nil) + #expect( + AppStoreIDInputParser.appStoreID( + from: "https://example.com/in/app/viewio-app/id6793099062" + ) == nil + ) + #expect( + AppStoreIDInputParser.appStoreID( + from: "https://apps.apple.com.example.com/app/id6793099062" + ) == nil + ) + #expect( + AppStoreIDInputParser.appStoreID( + from: "https://apps.apple.com/app/id9223372036854775808" + ) == nil + ) + } +}