Skip to content

Commit

Permalink
Merge pull request #197 from cocoatype/192-add-generic-web-deep-link
Browse files Browse the repository at this point in the history
Support web deep links on iOS
  • Loading branch information
Arclite authored Jul 26, 2024
2 parents 0bfd8d0 + 28d401c commit fe9473b
Show file tree
Hide file tree
Showing 21 changed files with 336 additions and 97 deletions.
79 changes: 43 additions & 36 deletions Highlighter.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -26,78 +26,78 @@
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "983FCA5AE887D0F8F0871B52",
"name" : "AlbumsDataTests"
"identifier" : "7C98246E8C096DC5B09B72A0",
"name" : "EditingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "FF81604FD0F405EF2D1CCC8B",
"name" : "ErrorHandlingTests"
"identifier" : "FFC529CC7C03FA34AB5A20B3",
"name" : "CoreTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "97F888315DF72D8A476D3EF9",
"name" : "LoggingTests"
"identifier" : "2B4661F6B9957A37CFC75AF1",
"name" : "ObservationsTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "DE152677D2304DC58677782F",
"name" : "DetectionsTests"
"identifier" : "E8CCAE4C408A436B716A3CDD",
"name" : "URLParsingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "F94B4B52A4D8EB0AFAC24486",
"name" : "RenderingTests"
"identifier" : "DE152677D2304DC58677782F",
"name" : "DetectionsTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "2B4661F6B9957A37CFC75AF1",
"name" : "ObservationsTests"
"identifier" : "AB94737BB65B38D6AD869B5F",
"name" : "ExportingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "4246008D146FDCE34C32DD40",
"name" : "ShortcutsTests"
"identifier" : "B5E4762CECC52CCD6E2B0990",
"name" : "RedactionsTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "AB94737BB65B38D6AD869B5F",
"name" : "ExportingTests"
"identifier" : "77D525B0D7498E1676A96CC5",
"name" : "UnpurchasedTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "B6DC51055F4C558AC1C5C422",
"name" : "GeometryTests"
"identifier" : "983FCA5AE887D0F8F0871B52",
"name" : "AlbumsDataTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "D511BD3660BA1CD7A06AEBC1",
"name" : "AutoRedactionsUITests"
"identifier" : "4246008D146FDCE34C32DD40",
"name" : "ShortcutsTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "2475A1B3C4289BC5302DD4C4",
"name" : "PurchaseMarketingTests"
"identifier" : "B6DC51055F4C558AC1C5C422",
"name" : "GeometryTests"
}
},
{
Expand All @@ -110,29 +110,29 @@
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "FFC529CC7C03FA34AB5A20B3",
"name" : "CoreTests"
"identifier" : "D511BD3660BA1CD7A06AEBC1",
"name" : "AutoRedactionsUITests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "55437D0117D8027C4D7EAECE",
"name" : "BrushesTests"
"identifier" : "CDF6EE4AB5693C601A1982E2",
"name" : "AlbumsUITests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "B5E4762CECC52CCD6E2B0990",
"name" : "RedactionsTests"
"identifier" : "55437D0117D8027C4D7EAECE",
"name" : "BrushesTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "CDF6EE4AB5693C601A1982E2",
"name" : "AlbumsUITests"
"identifier" : "4CA37C6F81D42C86B5E1F67B",
"name" : "AppRatingsTests"
}
},
{
Expand All @@ -145,22 +145,29 @@
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "4CA37C6F81D42C86B5E1F67B",
"name" : "AppRatingsTests"
"identifier" : "F94B4B52A4D8EB0AFAC24486",
"name" : "RenderingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "7C98246E8C096DC5B09B72A0",
"name" : "EditingTests"
"identifier" : "FF81604FD0F405EF2D1CCC8B",
"name" : "ErrorHandlingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "77D525B0D7498E1676A96CC5",
"name" : "UnpurchasedTests"
"identifier" : "2475A1B3C4289BC5302DD4C4",
"name" : "PurchaseMarketingTests"
}
},
{
"target" : {
"containerPath" : "container:Highlighter.xcodeproj",
"identifier" : "97F888315DF72D8A476D3EF9",
"name" : "LoggingTests"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import SafariServices

class WebViewController: SFSafariViewController {
init(url: URL) {
public class WebViewController: SFSafariViewController {
public init(url: URL) {
let configuration = SFSafariViewController.Configuration()
super.init(url: url, configuration: configuration)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import UIKit

enum CallbackAction {
public enum CallbackAction {
case edit(UIImage, URL?), open(UIImage)

var image: UIImage {
public var image: UIImage {
switch self {
case .edit(let image, _): return image
case .open(let image): return image
Expand Down
11 changes: 11 additions & 0 deletions Modules/Capabilities/URLParsing/Sources/URLParseResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Created by Geoff Pado on 7/25/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import UIKit

public enum URLParseResult {
case callbackAction(CallbackAction)
case image(URL)
case website(URL)
case invalid
}
30 changes: 30 additions & 0 deletions Modules/Capabilities/URLParsing/Sources/URLParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Created by Geoff Pado on 7/25/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import UIKit

public struct URLParser {
public init() {}

public func parse(_ url: URL) -> URLParseResult {
if let action = CallbackAction(url: url) {
return .callbackAction(action)
} else if url.isFileURL, FileManager.default.fileExists(atPath: url.path) {
return .image(url)
} else if let webURL = webURL(from: url) {
return .website(webURL)
} else {
return .invalid
}
}

func webURL(from url: URL) -> URL? {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "blackhighlighter.app"
else { return nil }

components.scheme = "https"

return components.url
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions Modules/Capabilities/URLParsing/Tests/CallbackActionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Created by Geoff Pado on 7/26/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import XCTest

@testable import URLParsing

class CallbackActionTests: XCTestCase {
func testCreatingOpenAction() throws {
let url = try CallbackActionURLGenerator().openURL(imageURL: imageURL)
let expectedImage = try XCTUnwrap(UIImage(contentsOfFile: imageURL.path))
let action = try XCTUnwrap(CallbackAction(url: url))
XCTAssert(action.isOpen)
XCTAssertEqual(action.image.size, expectedImage.size)
}

func testCreatingEditActionWithoutCallback() throws {
let url = try CallbackActionURLGenerator().editURL(imageURL: imageURL, successURL: nil)
let expectedImage = try XCTUnwrap(UIImage(contentsOfFile: imageURL.path))
let action = try XCTUnwrap(CallbackAction(url: url))
XCTAssert(action.isEdit)
XCTAssertNil(action.callbackURL)
XCTAssertEqual(action.image.size, expectedImage.size)
}

func testCreatingEditActionWithCallback() throws {
let successURL = try XCTUnwrap(URL(string: "https://blackhiglighter.app"))
let expectedImage = try XCTUnwrap(UIImage(contentsOfFile: imageURL.path))
let url = try CallbackActionURLGenerator().editURL(imageURL: imageURL, successURL: successURL)
let action = try XCTUnwrap(CallbackAction(url: url))
XCTAssert(action.isEdit)
XCTAssertEqual(action.callbackURL, successURL)
XCTAssertEqual(action.image.size, expectedImage.size)
}

func testCreatingNonCallbackAction() throws {
let url = try XCTUnwrap(URL(string: "highlighter://bad-host/"))
let action = CallbackAction(url: url)
XCTAssertNil(action)
}

func testCreatingInvalidCallbackAction() throws {
let url = try CallbackActionURLGenerator().url(action: "bad-action", imageURL: imageURL, successURL: nil)
let action = CallbackAction(url: url)
XCTAssertNil(action)
}
}

extension CallbackAction {
var isEdit: Bool {
switch self {
case .edit: true
case .open: false
}
}

var isOpen: Bool {
switch self {
case .open: true
case .edit: false
}
}

var callbackURL: URL? {
switch self {
case .edit(_, let url): url
case .open: nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Created by Geoff Pado on 7/26/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import XCTest

struct CallbackActionURLGenerator {
func openURL(imageURL: URL) throws -> URL {
try url(action: "open", imageURL: imageURL, successURL: nil)
}

func editURL(imageURL: URL, successURL: URL?) throws -> URL {
try url(action: "edit", imageURL: imageURL, successURL: successURL)
}

// MARK: Intermediate Builders

func url(action: String, imageURL: URL, successURL: URL?) throws -> URL {
let imageString = try imageDataString(forImageAt: imageURL)
var queryItems = [URLQueryItem(name: "imageData", value: imageString)]
if let successURL {
let encodedURL = try XCTUnwrap(successURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed))
queryItems.append(URLQueryItem(name: "x-success", value: encodedURL))
}
return try url(path: "/\(action)", queryItems: queryItems)
}

private func url(path: String, queryItems: [URLQueryItem]) throws -> URL {
var components = try XCTUnwrap(URLComponents(string: "highlighter://x-callback-url"))
components.path = path
components.queryItems = queryItems
return try XCTUnwrap(components.url)
}

private func imageDataString(forImageAt url: URL) throws -> String {
let imageData = try XCTUnwrap(FileManager.default.contents(atPath: url.path))
return imageData.base64EncodedString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Created by Geoff Pado on 7/26/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Foundation
import URLParsing

extension URLParseResult {
var imageURL: URL? {
switch self {
case .image(let imageURL): imageURL
case .callbackAction, .website, .invalid: nil
}
}

var isInvalid: Bool {
switch self {
case .invalid: true
case .callbackAction, .website, .image: false
}
}

var isCallbackAction: Bool {
switch self {
case .callbackAction: true
case .invalid, .website, .image: false
}
}

var webURL: URL? {
switch self {
case .website(let webURL): webURL
case .callbackAction, .image, .invalid: nil
}
}
}
Loading

0 comments on commit fe9473b

Please sign in to comment.