mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
[AltStoreCore] Caches Patreon session cookies from in-app browser
Allows us to download apps from locked Patreon posts.
This commit is contained in:
@@ -357,6 +357,7 @@
|
|||||||
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
|
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
|
||||||
D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
|
D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; };
|
||||||
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */; };
|
D52A2F972ACB40F700BDF8E3 /* Logger+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */; };
|
||||||
|
D52B4ABF2AF183F0005991C3 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52B4ABE2AF183F0005991C3 /* WebViewController.swift */; };
|
||||||
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
|
D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; };
|
||||||
D52DD35E2AAA89A600A7F2B6 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */; };
|
D52DD35E2AAA89A600A7F2B6 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */; };
|
||||||
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */; };
|
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */; };
|
||||||
@@ -976,6 +977,7 @@
|
|||||||
D51AD27C29356B7B00967AAA /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = "<group>"; };
|
D51AD27C29356B7B00967AAA /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = "<group>"; };
|
||||||
D51AD27D29356B7B00967AAA /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = "<group>"; };
|
D51AD27D29356B7B00967AAA /* ALTWrappedError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTWrappedError.m; sourceTree = "<group>"; };
|
||||||
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+AltStore.swift"; sourceTree = "<group>"; };
|
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+AltStore.swift"; sourceTree = "<group>"; };
|
||||||
|
D52B4ABE2AF183F0005991C3 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||||
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
|
D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = "<group>"; };
|
||||||
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
|
D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = "<group>"; };
|
||||||
D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailCollectionViewController.swift; sourceTree = "<group>"; };
|
D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
@@ -1496,6 +1498,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BF66EE8B2501AEB1007EE018 /* Keychain.swift */,
|
BF66EE8B2501AEB1007EE018 /* Keychain.swift */,
|
||||||
|
D52B4ABE2AF183F0005991C3 /* WebViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3062,6 +3065,7 @@
|
|||||||
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
|
||||||
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
|
||||||
BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */,
|
BF66EEA52501AEC5007EE018 /* Benefit.swift in Sources */,
|
||||||
|
D52B4ABF2AF183F0005991C3 /* WebViewController.swift in Sources */,
|
||||||
BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
|
BF66EED22501AECA007EE018 /* AltStore4ToAltStore5.xcmappingmodel in Sources */,
|
||||||
D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */,
|
D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */,
|
||||||
D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */,
|
D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ private extension PatreonViewController
|
|||||||
|
|
||||||
@IBAction func authenticate(_ sender: UIBarButtonItem)
|
@IBAction func authenticate(_ sender: UIBarButtonItem)
|
||||||
{
|
{
|
||||||
PatreonAPI.shared.authenticate { (result) in
|
PatreonAPI.shared.authenticate(presentingViewController: self) { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let account = try result.get()
|
let account = try result.get()
|
||||||
@@ -201,9 +201,12 @@ private extension PatreonViewController
|
|||||||
self.update()
|
self.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch ASWebAuthenticationSessionError.canceledLogin
|
catch is CancellationError
|
||||||
{
|
{
|
||||||
// Ignore
|
// Clear in-app browser cache in case they are signed into wrong account.
|
||||||
|
Task<Void, Never>.detached {
|
||||||
|
await PatreonAPI.shared.deleteAuthCookies()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
356
AltStoreCore/Components/WebViewController.swift
Normal file
356
AltStoreCore/Components/WebViewController.swift
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
//
|
||||||
|
// WebViewController.swift
|
||||||
|
// AltStoreCore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 10/31/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public protocol WebViewControllerDelegate: NSObject
|
||||||
|
{
|
||||||
|
func webViewControllerDidFinish(_ webViewController: WebViewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebViewController: UIViewController
|
||||||
|
{
|
||||||
|
//MARK: Public Properties
|
||||||
|
public weak var delegate: WebViewControllerDelegate?
|
||||||
|
|
||||||
|
// WKWebView used to display webpages
|
||||||
|
public private(set) var webView: WKWebView!
|
||||||
|
|
||||||
|
public private(set) lazy var backButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(WebViewController.goBack(_:)))
|
||||||
|
public private(set) lazy var forwardButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "chevron.forward"), style: .plain, target: self, action: #selector(WebViewController.goForward(_:)))
|
||||||
|
public private(set) lazy var shareButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(WebViewController.shareLink(_:)))
|
||||||
|
|
||||||
|
public private(set) lazy var reloadButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(WebViewController.refresh(_:)))
|
||||||
|
public private(set) lazy var stopLoadingButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(WebViewController.refresh(_:)))
|
||||||
|
|
||||||
|
public private(set) lazy var doneButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(WebViewController.dismissWebViewController(_:)))
|
||||||
|
|
||||||
|
//MARK: Private Properties
|
||||||
|
private let progressView = UIProgressView()
|
||||||
|
private lazy var refreshButton: UIBarButtonItem = self.reloadButton
|
||||||
|
|
||||||
|
private let initialReqest: URLRequest?
|
||||||
|
private var ignoreUpdateProgress: Bool = false
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
public required init(request: URLRequest?, configuration: WKWebViewConfiguration = WKWebViewConfiguration())
|
||||||
|
{
|
||||||
|
self.initialReqest = request
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
self.webView = WKWebView(frame: CGRectZero, configuration: configuration)
|
||||||
|
self.webView.allowsBackForwardNavigationGestures = true
|
||||||
|
|
||||||
|
self.progressView.progressViewStyle = .bar
|
||||||
|
self.progressView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.progressView.progress = 0.5
|
||||||
|
self.progressView.alpha = 0.0
|
||||||
|
self.progressView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(url: URL?, configuration: WKWebViewConfiguration = WKWebViewConfiguration())
|
||||||
|
{
|
||||||
|
if let url
|
||||||
|
{
|
||||||
|
let request = URLRequest(url: url)
|
||||||
|
self.init(request: request, configuration: configuration)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.init(request: nil, configuration: configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UIViewController
|
||||||
|
|
||||||
|
public override func loadView()
|
||||||
|
{
|
||||||
|
self.preparePipeline()
|
||||||
|
|
||||||
|
if let request = self.initialReqest
|
||||||
|
{
|
||||||
|
self.webView.load(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.view = self.webView
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.navigationController?.isModalInPresentation = true
|
||||||
|
self.navigationController?.view.tintColor = .altPrimary
|
||||||
|
|
||||||
|
if let navigationBar = self.navigationController?.navigationBar
|
||||||
|
{
|
||||||
|
navigationBar.scrollEdgeAppearance = navigationBar.standardAppearance
|
||||||
|
}
|
||||||
|
|
||||||
|
if let toolbar = self.navigationController?.toolbar, #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
toolbar.scrollEdgeAppearance = toolbar.standardAppearance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewIsAppearing(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewIsAppearing(animated)
|
||||||
|
|
||||||
|
if self.webView.estimatedProgress < 1.0
|
||||||
|
{
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { context in
|
||||||
|
self.showProgressBar(animated: true)
|
||||||
|
}) { context in
|
||||||
|
if context.isCancelled
|
||||||
|
{
|
||||||
|
self.hideProgressBar(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationController?.setToolbarHidden(false, animated: false)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewWillDisappear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
var shouldHideToolbarItems = true
|
||||||
|
|
||||||
|
if let toolbarItems = self.navigationController?.topViewController?.toolbarItems
|
||||||
|
{
|
||||||
|
if toolbarItems.count > 0
|
||||||
|
{
|
||||||
|
shouldHideToolbarItems = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldHideToolbarItems
|
||||||
|
{
|
||||||
|
self.navigationController?.setToolbarHidden(true, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transitionCoordinator?.animate(alongsideTransition: { context in
|
||||||
|
self.hideProgressBar(animated: true)
|
||||||
|
}) { (context) in
|
||||||
|
if context.isCancelled && self.webView.estimatedProgress < 1.0
|
||||||
|
{
|
||||||
|
self.showProgressBar(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func didMove(toParent parent: UIViewController?)
|
||||||
|
{
|
||||||
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
|
if parent == nil
|
||||||
|
{
|
||||||
|
self.webView.stopLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit
|
||||||
|
{
|
||||||
|
self.webView.stopLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension WebViewController
|
||||||
|
{
|
||||||
|
func preparePipeline()
|
||||||
|
{
|
||||||
|
self.webView.publisher(for: \.title, options: [.initial, .new])
|
||||||
|
.sink { [weak self] title in
|
||||||
|
self?.title = title
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
self.webView.publisher(for: \.estimatedProgress, options: [.new])
|
||||||
|
.sink { [weak self] progress in
|
||||||
|
self?.updateProgress(progress)
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
|
||||||
|
Publishers.Merge3(
|
||||||
|
self.webView.publisher(for: \.isLoading, options: [.new]),
|
||||||
|
self.webView.publisher(for: \.canGoBack, options: [.new]),
|
||||||
|
self.webView.publisher(for: \.canGoForward, options: [.new])
|
||||||
|
)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.update()
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
if self.webView.isLoading
|
||||||
|
{
|
||||||
|
self.refreshButton = self.stopLoadingButton
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.refreshButton = self.reloadButton
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backButton.isEnabled = self.webView.canGoBack
|
||||||
|
self.forwardButton.isEnabled = self.webView.canGoForward
|
||||||
|
|
||||||
|
self.navigationItem.leftBarButtonItem = self.doneButton
|
||||||
|
self.navigationItem.rightBarButtonItem = self.refreshButton
|
||||||
|
|
||||||
|
self.toolbarItems = [self.backButton, .fixedSpace(70), self.forwardButton, .flexibleSpace(), self.shareButton]
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProgress(_ progress: Double)
|
||||||
|
{
|
||||||
|
if self.progressView.isHidden
|
||||||
|
{
|
||||||
|
self.showProgressBar(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ignoreUpdateProgress
|
||||||
|
{
|
||||||
|
self.ignoreUpdateProgress = false
|
||||||
|
self.hideProgressBar(animated: true)
|
||||||
|
}
|
||||||
|
else if progress < Double(self.progressView.progress)
|
||||||
|
{
|
||||||
|
// If progress is less than self.progressView.progress, another webpage began to load before the first one completed
|
||||||
|
// In this case, we set the progress back to 0.0, and then wait until the next updateProgress, because it results in a much better animation
|
||||||
|
|
||||||
|
self.progressView.setProgress(0.0, animated: false)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UIView.animate(withDuration: 0.4, animations: {
|
||||||
|
self.progressView.setProgress(Float(progress), animated: true)
|
||||||
|
}, completion: { (finished) in
|
||||||
|
if progress == 1.0
|
||||||
|
{
|
||||||
|
// This delay serves two purposes. One, it keeps the progress bar on screen just a bit longer so it doesn't appear to disappear too quickly.
|
||||||
|
// Two, it allows us to prevent the progress bar from disappearing if the user actually started loading another webpage before the current one finished loading.
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||||
|
if self.webView.estimatedProgress == 1.0
|
||||||
|
{
|
||||||
|
self.hideProgressBar(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showProgressBar(animated: Bool)
|
||||||
|
{
|
||||||
|
let navigationBarBounds = self.navigationController?.navigationBar.bounds ?? .zero
|
||||||
|
self.progressView.frame = CGRect(x: 0, y: navigationBarBounds.height - self.progressView.bounds.height, width: navigationBarBounds.width, height: self.progressView.bounds.height)
|
||||||
|
|
||||||
|
self.navigationController?.navigationBar.addSubview(self.progressView)
|
||||||
|
|
||||||
|
self.progressView.setProgress(Float(self.webView.estimatedProgress), animated: false)
|
||||||
|
self.progressView.isHidden = false
|
||||||
|
|
||||||
|
if animated
|
||||||
|
{
|
||||||
|
UIView.animate(withDuration: 0.4) {
|
||||||
|
self.progressView.alpha = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.progressView.alpha = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideProgressBar(animated: Bool)
|
||||||
|
{
|
||||||
|
if animated
|
||||||
|
{
|
||||||
|
UIView.animate(withDuration: 0.4, animations: {
|
||||||
|
self.progressView.alpha = 0.0
|
||||||
|
}, completion: { (finished) in
|
||||||
|
self.progressView.setProgress(0.0, animated: false)
|
||||||
|
self.progressView.isHidden = true
|
||||||
|
self.progressView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.progressView.alpha = 0.0
|
||||||
|
|
||||||
|
// Completion
|
||||||
|
self.progressView.setProgress(0.0, animated: false)
|
||||||
|
self.progressView.isHidden = true
|
||||||
|
self.progressView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private extension WebViewController
|
||||||
|
{
|
||||||
|
func goBack(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
self.webView.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
func goForward(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
self.webView.goForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
if self.webView.isLoading
|
||||||
|
{
|
||||||
|
self.ignoreUpdateProgress = true
|
||||||
|
self.webView.stopLoading()
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if let initialRequest = self.initialReqest, self.webView.url == nil && self.webView.backForwardList.backList.count == 0
|
||||||
|
{
|
||||||
|
self.webView.load(initialRequest)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.webView.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shareLink(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
let url = self.webView.url ?? (NSURL() as URL)
|
||||||
|
|
||||||
|
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||||
|
activityViewController.modalPresentationStyle = .popover
|
||||||
|
activityViewController.popoverPresentationController?.barButtonItem = sender
|
||||||
|
self.present(activityViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissWebViewController(_ sender: UIBarButtonItem)
|
||||||
|
{
|
||||||
|
self.delegate?.webViewControllerDidFinish(self)
|
||||||
|
|
||||||
|
self.parent?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import WebKit
|
||||||
|
|
||||||
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
|
private let clientID = "ZMx0EGUWe4TVWYXNZZwK_fbIK5jHFVWoUf1Qb-sqNXmT-YzAGwDPxxq7ak3_W5Q2"
|
||||||
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
|
private let clientSecret = "1hktsZB89QyN69cB4R0tu55R4TCPQGXxvebYUUh7Y-5TLSnRswuxs6OUjdJ74IJt"
|
||||||
@@ -58,6 +59,10 @@ public class PatreonAPI: NSObject
|
|||||||
private let session = URLSession(configuration: .ephemeral)
|
private let session = URLSession(configuration: .ephemeral)
|
||||||
private let baseURL = URL(string: "https://www.patreon.com/")!
|
private let baseURL = URL(string: "https://www.patreon.com/")!
|
||||||
|
|
||||||
|
private var authHandlers = [(Result<PatreonAccount, Swift.Error>) -> Void]()
|
||||||
|
private var authContinuation: CheckedContinuation<URL, Error>?
|
||||||
|
private weak var webViewController: WebViewController?
|
||||||
|
|
||||||
private override init()
|
private override init()
|
||||||
{
|
{
|
||||||
super.init()
|
super.init()
|
||||||
@@ -66,19 +71,40 @@ public class PatreonAPI: NSObject
|
|||||||
|
|
||||||
public extension PatreonAPI
|
public extension PatreonAPI
|
||||||
{
|
{
|
||||||
func authenticate(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
func authenticate(presentingViewController: UIViewController, completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||||
{
|
{
|
||||||
var components = URLComponents(string: "/oauth2/authorize")!
|
Task<Void, Never>.detached { @MainActor in
|
||||||
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
|
guard self.authHandlers.isEmpty else {
|
||||||
URLQueryItem(name: "client_id", value: clientID),
|
self.authHandlers.append(completion)
|
||||||
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")]
|
return
|
||||||
|
}
|
||||||
let requestURL = components.url(relativeTo: self.baseURL)!
|
|
||||||
|
self.authHandlers.append(completion)
|
||||||
self.authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { (callbackURL, error) in
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let callbackURL = try Result(callbackURL, error).get()
|
var components = URLComponents(string: "/oauth2/authorize")!
|
||||||
|
components.queryItems = [URLQueryItem(name: "response_type", value: "code"),
|
||||||
|
URLQueryItem(name: "client_id", value: clientID),
|
||||||
|
URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore"),
|
||||||
|
URLQueryItem(name: "scope", value: "identity identity[email] identity.memberships campaigns.posts")]
|
||||||
|
|
||||||
|
let requestURL = components.url(relativeTo: self.baseURL)
|
||||||
|
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
configuration.setURLSchemeHandler(self, forURLScheme: "altstore")
|
||||||
|
configuration.websiteDataStore = .default()
|
||||||
|
|
||||||
|
let webViewController = WebViewController(url: requestURL, configuration: configuration)
|
||||||
|
webViewController.delegate = self
|
||||||
|
self.webViewController = webViewController
|
||||||
|
|
||||||
|
let callbackURL = try await withCheckedThrowingContinuation { continuation in
|
||||||
|
self.authContinuation = continuation
|
||||||
|
|
||||||
|
let navigationController = UINavigationController(rootViewController: webViewController)
|
||||||
|
presentingViewController.present(navigationController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
|
||||||
@@ -86,26 +112,45 @@ public extension PatreonAPI
|
|||||||
let code = codeQueryItem.value
|
let code = codeQueryItem.value
|
||||||
else { throw PatreonAPIError(.unknown) }
|
else { throw PatreonAPIError(.unknown) }
|
||||||
|
|
||||||
self.fetchAccessToken(oauthCode: code) { (result) in
|
let (accessToken, refreshToken) = try await withCheckedThrowingContinuation { continuation in
|
||||||
switch result
|
self.fetchAccessToken(oauthCode: code) { result in
|
||||||
|
continuation.resume(with: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keychain.shared.patreonAccessToken = accessToken
|
||||||
|
Keychain.shared.patreonRefreshToken = refreshToken
|
||||||
|
|
||||||
|
let patreonAccount = try await withCheckedThrowingContinuation { continuation in
|
||||||
|
self.fetchAccount { result in
|
||||||
|
let result = result.map { AsyncManaged(wrappedValue: $0) }
|
||||||
|
continuation.resume(with: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.saveAuthCookies()
|
||||||
|
|
||||||
|
await patreonAccount.perform { patreonAccount in
|
||||||
|
for callback in self.authHandlers
|
||||||
{
|
{
|
||||||
case .failure(let error): completion(.failure(error))
|
callback(.success(patreonAccount))
|
||||||
case .success((let accessToken, let refreshToken)):
|
|
||||||
Keychain.shared.patreonAccessToken = accessToken
|
|
||||||
Keychain.shared.patreonRefreshToken = refreshToken
|
|
||||||
|
|
||||||
self.fetchAccount(completion: completion)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
completion(.failure(error))
|
for callback in self.authHandlers
|
||||||
|
{
|
||||||
|
callback(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.authHandlers = []
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
self.webViewController?.dismiss(animated: true)
|
||||||
|
self.webViewController = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.authenticationSession?.presentationContextProvider = self
|
|
||||||
self.authenticationSession?.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
func fetchAccount(completion: @escaping (Result<PatreonAccount, Swift.Error>) -> Void)
|
||||||
@@ -205,7 +250,10 @@ public extension PatreonAPI
|
|||||||
Keychain.shared.patreonRefreshToken = nil
|
Keychain.shared.patreonRefreshToken = nil
|
||||||
Keychain.shared.patreonAccountID = nil
|
Keychain.shared.patreonAccountID = nil
|
||||||
|
|
||||||
completion(.success(()))
|
Task<Void, Never>.detached {
|
||||||
|
await self.deleteAuthCookies()
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -239,6 +287,56 @@ public extension PatreonAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PatreonAPI
|
||||||
|
{
|
||||||
|
private func saveAuthCookies() async
|
||||||
|
{
|
||||||
|
let cookieStore = await MainActor.run { WKWebsiteDataStore.default().httpCookieStore } // Must access from main actor
|
||||||
|
|
||||||
|
let cookies = await cookieStore.allCookies()
|
||||||
|
for cookie in cookies where cookie.domain.lowercased().hasSuffix("patreon.com")
|
||||||
|
{
|
||||||
|
Logger.main.debug("Saving Patreon cookie \(cookie.name, privacy: .public): \(cookie.value, privacy: .private(mask: .hash)) (Expires: \(cookie.expiresDate?.description ?? "nil", privacy: .public))")
|
||||||
|
HTTPCookieStorage.shared.setCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func deleteAuthCookies() async
|
||||||
|
{
|
||||||
|
Logger.main.info("Clearing Patreon cookie cache...")
|
||||||
|
|
||||||
|
let cookieStore = await MainActor.run { WKWebsiteDataStore.default().httpCookieStore } // Must access from main actor
|
||||||
|
|
||||||
|
if let cookies = HTTPCookieStorage.shared.cookies(for: URL(string: "https://www.patreon.com")!)
|
||||||
|
{
|
||||||
|
for cookie in cookies
|
||||||
|
{
|
||||||
|
Logger.main.debug("Deleting Patreon cookie \(cookie.name, privacy: .public) (Expires: \(cookie.expiresDate?.description ?? "nil", privacy: .public))")
|
||||||
|
|
||||||
|
await cookieStore.deleteCookie(cookie)
|
||||||
|
HTTPCookieStorage.shared.deleteCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.main.info("Cleared Patreon cookie cache!")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.main.info("No Patreon cookies to clear.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PatreonAPI: WebViewControllerDelegate
|
||||||
|
{
|
||||||
|
public func webViewControllerDidFinish(_ webViewController: WebViewController)
|
||||||
|
{
|
||||||
|
guard let authContinuation else { return }
|
||||||
|
self.authContinuation = nil
|
||||||
|
|
||||||
|
authContinuation.resume(throwing: CancellationError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension PatreonAPI
|
private extension PatreonAPI
|
||||||
{
|
{
|
||||||
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void)
|
func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void)
|
||||||
@@ -366,25 +464,25 @@ private extension PatreonAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PatreonAPI: ASWebAuthenticationPresentationContextProviding
|
extension PatreonAPI: WKURLSchemeHandler
|
||||||
{
|
{
|
||||||
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
|
public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask)
|
||||||
{
|
{
|
||||||
//TODO: Properly support multiple scenes.
|
guard let authContinuation else { return }
|
||||||
|
self.authContinuation = nil
|
||||||
guard let windowScene = UIApplication.alt_shared?.connectedScenes.lazy.compactMap({ $0 as? UIWindowScene }).first else { return UIWindow() }
|
|
||||||
|
if let callbackURL = urlSchemeTask.request.url
|
||||||
if #available(iOS 15, *), let keyWindow = windowScene.keyWindow
|
|
||||||
{
|
{
|
||||||
return keyWindow
|
authContinuation.resume(returning: callbackURL)
|
||||||
}
|
}
|
||||||
else if let delegate = windowScene.delegate as? UIWindowSceneDelegate,
|
else
|
||||||
let optionalWindow = delegate.window,
|
|
||||||
let window = optionalWindow
|
|
||||||
{
|
{
|
||||||
return window
|
authContinuation.resume(throwing: URLError(.badURL))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return UIWindow()
|
|
||||||
|
public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask)
|
||||||
|
{
|
||||||
|
Logger.main.debug("WKWebView stopped handling url scheme.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user