Enabling Extensions now possible in SwiftUI

This commit is contained in:
Stuart Breckenridge
2022-12-19 12:14:56 +08:00
parent dbef68c8f8
commit ef6b90d594
10 changed files with 299 additions and 268 deletions

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct ExtensionInspectorView: View {
@Environment(\.dismiss) var dismiss
@State private var showDeactivateConfirmation: Bool = false
var extensionPoint: ExtensionPoint?
@@ -25,14 +26,15 @@ struct ExtensionInspectorView: View {
Button(role: .destructive) {
showDeactivateConfirmation = true
} label: {
Text("DEACTIVATE_EXTENSION_TITLE", tableName: "Settings")
Text("DEACTIVATE_EXTENSION_BUTTON_TITLE", tableName: "Buttons")
}
.confirmationDialog(Text("DEACTIVATE_EXTENSION_TITLE", tableName: "Settings") , isPresented: $showDeactivateConfirmation, titleVisibility: .visible) {
Button(role: .destructive) {
ExtensionPointManager.shared.deactivateExtensionPoint(extensionPoint!.extensionPointID)
dismiss()
} label: {
Text("DEACTIVATE_BUTTON_TITLE", tableName: "Buttons")
Text("DEACTIVATE_EXTENSION_BUTTON_TITLE", tableName: "Buttons")
}
Button(role: .cancel) {

View File

@@ -1,63 +0,0 @@
//
// AddExtensionPointViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/16/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
protocol AddExtensionPointDismissDelegate: UIViewController {
func dismiss()
}
class AddExtensionPointViewController: UITableViewController, AddExtensionPointDismissDelegate {
private var availableExtensionPointTypes = [ExtensionPoint.Type]()
override func viewDidLoad() {
super.viewDidLoad()
availableExtensionPointTypes = ExtensionPointManager.shared.availableExtensionPointTypes.sorted(by: { $0.title < $1.title })
}
override func numberOfSections(in tableView: UITableView) -> Int {
1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return availableExtensionPointTypes.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsExtensionTableViewCell", for: indexPath) as! SettingsComboTableViewCell
let extensionPointType = availableExtensionPointTypes[indexPath.row]
cell.comboNameLabel?.text = extensionPointType.title
cell.comboImage?.image = extensionPointType.image
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return NSLocalizedString("Feed Provider", comment: "Feed Provider Header")
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return NSLocalizedString("Feed Providers allow you to subscribe to some pages as if they were RSS feeds.", comment: "Feed Provider Footer")
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let navController = UIStoryboard.settings.instantiateViewController(withIdentifier: "EnableExtensionPointNavigationViewController") as! UINavigationController
navController.modalPresentationStyle = .currentContext
let enableViewController = navController.topViewController as! EnableExtensionPointViewController
enableViewController.delegate = self
enableViewController.extensionPointType = availableExtensionPointTypes[indexPath.row]
present(navController, animated: true)
}
func dismiss() {
navigationController?.popViewController(animated: false)
}
}

View File

@@ -1,192 +0,0 @@
//
// EnableExtensionPointViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/16/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
import AuthenticationServices
import Account
import OAuthSwift
import Secrets
import SwiftUI
struct EnableExtensionPointViewWrapper: UIViewControllerRepresentable {
var extensionPoint: ExtensionPoint.Type?
func makeUIViewController(context: Context) -> EnableExtensionPointViewController {
let controller = UIStoryboard.settings.instantiateViewController(withIdentifier: "EnableExtensionPointViewController") as! EnableExtensionPointViewController
controller.extensionPointType = extensionPoint
return controller
}
func updateUIViewController(_ uiViewController: EnableExtensionPointViewController, context: Context) {
}
typealias UIViewControllerType = EnableExtensionPointViewController
}
class EnableExtensionPointViewController: UITableViewController {
@IBOutlet weak var extensionDescription: UILabel!
private var callbackURL: URL? = nil
private var oauth: OAuthSwift?
weak var delegate: AddExtensionPointDismissDelegate?
var extensionPointType: ExtensionPoint.Type?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = extensionPointType?.title
extensionDescription.attributedText = extensionPointType?.description
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
}
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
delegate?.dismiss()
}
@IBAction func enable(_ sender: Any) {
guard let extensionPointType = extensionPointType else { return }
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
enableOauth1(oauth1)
} else if let oauth2 = extensionPointType as? OAuth2SwiftProvider.Type {
enableOauth2(oauth2)
} else {
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType) { result in
if case .failure(let error) = result {
self.presentError(error)
}
self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss()
}
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
headerView.imageView.image = extensionPointType?.image
return headerView
} else {
return super.tableView(tableView, viewForHeaderInSection: section)
}
}
}
extension EnableExtensionPointViewController: OAuthSwiftURLHandlerType {
public func handle(_ url: URL) {
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL!.scheme, completionHandler: { (url, error) in
if let callbackedURL = url {
OAuth1Swift.handle(url: callbackedURL)
}
guard let error = error else { return }
self.oauth?.cancel()
self.oauth = nil
DispatchQueue.main.async {
self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss()
}
if case ASWebAuthenticationSessionError.canceledLogin = error {
print("Login cancelled.")
} else {
self.presentError(error)
}
})
session.presentationContextProvider = self
if !session.start() {
print("Session failed to start!!!")
}
}
}
extension EnableExtensionPointViewController: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return view.window!
}
}
private extension EnableExtensionPointViewController {
func enableOauth1(_ provider: OAuth1SwiftProvider.Type) {
callbackURL = provider.callbackURL
let oauth1 = provider.oauth1Swift
self.oauth = oauth1
oauth1.authorizeURLHandler = self
oauth1.authorize(withCallbackURL: callbackURL!) { [weak self] result in
guard let self = self, let extensionPointType = self.extensionPointType else { return }
switch result {
case .success(let tokenSuccess):
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
if case .failure(let error) = result {
self.presentError(error)
}
self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss()
}
case .failure(let oauthSwiftError):
self.presentError(oauthSwiftError)
}
self.oauth?.cancel()
self.oauth = nil
}
}
func enableOauth2(_ provider: OAuth2SwiftProvider.Type) {
callbackURL = provider.callbackURL
let oauth2 = provider.oauth2Swift
self.oauth = oauth2
oauth2.authorizeURLHandler = self
let oauth2Vars = provider.oauth2Vars
oauth2.authorize(withCallbackURL: callbackURL!, scope: oauth2Vars.scope, state: oauth2Vars.state, parameters: oauth2Vars.params) { [weak self] result in
guard let self = self, let extensionPointType = self.extensionPointType else { return }
switch result {
case .success(let tokenSuccess):
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
if case .failure(let error) = result {
self.presentError(error)
}
self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss()
}
case .failure(let oauthSwiftError):
self.presentError(oauthSwiftError)
}
self.oauth?.cancel()
self.oauth = nil
}
}
}

View File

@@ -15,7 +15,8 @@ public final class AddAccountListViewModel: ObservableObject, OAuthAccountAuthor
@Published public var showAddAccountSheet: (Bool, accountType: AccountType) = (false, .onMyMac)
public var webAccountTypes: [AccountType] {
if AppDefaults.shared.isDeveloperBuild {
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader].filter({ $0.isDeveloperRestricted == false })
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
//.filter({ $0.isDeveloperRestricted == false })
} else {
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
}

View File

@@ -21,8 +21,7 @@ struct AddExtensionListView: View {
footer: Text("FEED_PROVIDER_FOOTER", tableName: "Settings")) {
ForEach(0..<availableExtensionPointTypes.count, id: \.self) { i in
NavigationLink {
EnableExtensionPointViewWrapper(extensionPoint: availableExtensionPointTypes[i])
.edgesIgnoringSafeArea(.all)
EnableExtensionPointView(extensionPoint: availableExtensionPointTypes[i])
} label: {
Image(uiImage: availableExtensionPointTypes[i].image)
.resizable()

View File

@@ -0,0 +1,66 @@
//
// EnableExtensionPointView.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 19/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
struct EnableExtensionPointView: View {
@Environment(\.dismiss) var dismiss
@StateObject private var viewModel = EnableExtensionViewModel()
@State private var extensionError: (Error?, Bool) = (nil, false)
var extensionPoint: ExtensionPoint.Type
var body: some View {
Form {
ExtensionSectionHeader(extensionPoint: extensionPoint)
Section(footer: extensionExplainer) {}
Section { enableButton }
}
.alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $extensionError.1, actions: {
Button(action: {}, label: { Text("DISMISS_BUTTON_TITLE", tableName: "Buttons") })
}, message: {
Text(extensionError.0?.localizedDescription ?? "Unknown Error")
})
.navigationTitle(extensionPoint.title)
.navigationBarTitleDisplayMode(.inline)
.dismissOnExternalContextLaunch()
.onReceive(NotificationCenter.default.publisher(for: .ActiveExtensionPointsDidChange)) { _ in
dismiss()
}
.edgesIgnoringSafeArea(.bottom)
}
var extensionExplainer: some View {
Text(extensionPoint.description.string)
.multilineTextAlignment(.center)
}
var enableButton: some View {
Button {
Task {
viewModel.configure(extensionPoint)
do {
try await viewModel.enableExtension()
} catch {
extensionError = (error, true)
}
}
} label: {
HStack {
Spacer()
Text("ENABLE_EXTENSION_BUTTON_TITLE", tableName: "Buttons")
Spacer()
}
}
}
}

View File

@@ -0,0 +1,182 @@
//
// EnableExtensionViewModel.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 19/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import Foundation
import AuthenticationServices
import Account
import OAuthSwift
import Secrets
import RSCore
@MainActor
public final class EnableExtensionViewModel: NSObject, ObservableObject, OAuthSwiftURLHandlerType, ASWebAuthenticationPresentationContextProviding, Logging {
private var extensionPointType: ExtensionPoint.Type?
private var oauth: OAuthSwift?
private var callbackURL: URL? = nil
func configure(_ extensionPointType: ExtensionPoint.Type) {
self.extensionPointType = extensionPointType
}
func enableExtension() async throws {
guard let extensionPointType = extensionPointType else { return }
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
try await enableOAuth1(oauth1)
} else if let oauth2 = extensionPointType as? OAuth2SwiftProvider.Type {
try await enableOAuth2(oauth2)
} else {
try await activateExtensionPoint(extensionPointType)
}
}
private func activateExtensionPoint(_ point: ExtensionPoint.Type) async throws {
return try await withCheckedThrowingContinuation { continuation in
ExtensionPointManager.shared.activateExtensionPoint(point) { result in
switch result {
case .success(_):
continuation.resume()
return
case .failure(let failure):
continuation.resume(throwing: failure)
return
}
}
}
}
// MARK: Enable OAuth
private func enableOAuth1(_ provider: OAuth1SwiftProvider.Type) async throws {
callbackURL = provider.callbackURL
let oauth1 = provider.oauth1Swift
self.oauth = oauth1
oauth1.authorizeURLHandler = self
return try await withCheckedThrowingContinuation { continuation in
oauth1.authorize(withCallbackURL: callbackURL!) { [weak self] result in
guard let self = self, let extensionPointType = self.extensionPointType else { return }
switch result {
case .success(let tokenSuccess):
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
switch result {
case .success(_):
continuation.resume()
return
case .failure(let failure):
continuation.resume(throwing: failure)
return
}
}
case .failure(let error):
continuation.resume(throwing: error)
return
}
self.oauth?.cancel()
self.oauth = nil
}
continuation.resume()
}
}
private func enableOAuth2(_ provider: OAuth2SwiftProvider.Type) async throws {
callbackURL = provider.callbackURL
let oauth2 = provider.oauth2Swift
self.oauth = oauth2
oauth2.authorizeURLHandler = self
let oauth2Vars = provider.oauth2Vars
return try await withCheckedThrowingContinuation { continuation in
oauth2.authorize(withCallbackURL: callbackURL!, scope: oauth2Vars.scope, state: oauth2Vars.state, parameters: oauth2Vars.params) { [weak self] result in
guard let self = self, let extensionPointType = self.extensionPointType else { return }
switch result {
case .success(let tokenSuccess):
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { [weak self] result in
switch result {
case .success(_):
self?.logger.debug("Enabled extension successfully.")
case .failure(let failure):
continuation.resume(throwing: failure)
return
}
}
case .failure(let oauthSwiftError):
continuation.resume(throwing: oauthSwiftError)
return
}
self.oauth?.cancel()
self.oauth = nil
}
continuation.resume()
}
}
public func handle(_ url: URL) {
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURL!.scheme, completionHandler: { (url, error) in
if let callbackedURL = url {
OAuth1Swift.handle(url: callbackedURL)
}
guard let error = error else { return }
self.oauth?.cancel()
self.oauth = nil
DispatchQueue.main.async {
//self.dismiss(animated: true, completion: nil)
//self.delegate?.dismiss()
}
if case ASWebAuthenticationSessionError.canceledLogin = error {
print("Login cancelled.")
} else {
//self.presentError(error)
}
})
session.presentationContextProvider = self
if !session.start() {
print("Session failed to start!!!")
}
}
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return rootViewController!.view.window!
}
public var rootViewController: UIViewController? {
var currentKeyWindow: UIWindow? {
UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.map { $0 as? UIWindowScene }
.compactMap { $0 }
.first?.windows
.filter { $0.isKeyWindow }
.first
}
var rootViewController: UIViewController? {
currentKeyWindow?.rootViewController
}
return rootViewController
}
}

View File

@@ -0,0 +1,30 @@
//
// ExtensionSectionHeader.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 19/12/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import SwiftUI
import Account
struct ExtensionSectionHeader: View {
var extensionPoint: ExtensionPoint.Type
var body: some View {
Section(header: headerImage) {}
}
var headerImage: some View {
HStack {
Spacer()
Image(uiImage: extensionPoint.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 48, height: 48)
Spacer()
}
}
}