From a7896bbcbb755b5c90dc6a982f06c970fe6f39bc Mon Sep 17 00:00:00 2001 From: Tristan Warner-Smith Date: Thu, 29 Feb 2024 10:44:48 +0000 Subject: [PATCH 1/4] Introduces MagicViewHostProvider that allows external users to give Magic the view controller to add to. Defaults to the top-most view controller of the keywindow. --- Sources/MagicSDK/Core/Magic.swift | 38 +++--- .../Core/Provider/MagicViewHostProvider.swift | 39 ++++++ .../MagicSDK/Core/Provider/RpcProvider.swift | 10 +- .../Core/Relayer/WebViewController.swift | 123 +++++++++--------- 4 files changed, 125 insertions(+), 85 deletions(-) create mode 100644 Sources/MagicSDK/Core/Provider/MagicViewHostProvider.swift diff --git a/Sources/MagicSDK/Core/Magic.swift b/Sources/MagicSDK/Core/Magic.swift index 08e21fd..2a6cca5 100644 --- a/Sources/MagicSDK/Core/Magic.swift +++ b/Sources/MagicSDK/Core/Magic.swift @@ -13,14 +13,14 @@ import WebKit public class Magic: NSObject { // MARK: - Log Message Warning public let MA_EXTENSION_ONLY_MSG = "This extension only works with Magic Auth API Keys" - + // MARK: - Modules public let user: UserModule public let auth: AuthModule public let wallet: WalletModule - + // MARK: - Property - public var rpcProvider: RpcProvider + public var rpcProvider: RpcProvider /// Shared instance of `Magic` public static var shared: Magic! @@ -32,28 +32,30 @@ public class Magic: NSObject { /// - Parameters: /// - apiKey: Your client ID. From https://dashboard.Magic.com /// - ethNetwork: Etherum Network setting (ie. mainnet or goerli) - /// - customNode: A custom RPC node - public convenience init(apiKey: String, ethNetwork: EthNetwork, locale: String = Locale.current.identifier) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: ethNetwork, locale: locale)) + /// - customNode: A custom RPC node + /// - viewHostProvider: An optional `UIViewController` provider for login views to embed within + public convenience init(apiKey: String, ethNetwork: EthNetwork, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: ethNetwork, locale: locale), viewHostProvider: viewHostProvider) } - public convenience init(apiKey: String, customNode: CustomNodeConfiguration, locale: String = Locale.current.identifier) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, customNode: customNode, locale: locale)) + public convenience init(apiKey: String, customNode: CustomNodeConfiguration, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, customNode: customNode, locale: locale), viewHostProvider: viewHostProvider) } - public convenience init(apiKey: String, locale: String = Locale.current.identifier) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: EthNetwork.mainnet, locale: locale)) + public convenience init(apiKey: String, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: EthNetwork.mainnet, locale: locale), viewHostProvider: viewHostProvider) } /// Core constructor - private init(urlBuilder: URLBuilder) { - self.rpcProvider = RpcProvider(urlBuilder: urlBuilder) - - self.user = UserModule(rpcProvider: self.rpcProvider) - self.auth = AuthModule(rpcProvider: self.rpcProvider) - self.wallet = WalletModule(rpcProvider: self.rpcProvider) - - super.init() + private init(urlBuilder: URLBuilder, viewHostProvider: MagicViewHostProviding?) { + let viewHostProvider = viewHostProvider ?? MagicViewHostProvider() + self.rpcProvider = RpcProvider(urlBuilder: urlBuilder, viewHostProvider: viewHostProvider) + + self.user = UserModule(rpcProvider: self.rpcProvider) + self.auth = AuthModule(rpcProvider: self.rpcProvider) + self.wallet = WalletModule(rpcProvider: self.rpcProvider) + + super.init() } } diff --git a/Sources/MagicSDK/Core/Provider/MagicViewHostProvider.swift b/Sources/MagicSDK/Core/Provider/MagicViewHostProvider.swift new file mode 100644 index 0000000..955d554 --- /dev/null +++ b/Sources/MagicSDK/Core/Provider/MagicViewHostProvider.swift @@ -0,0 +1,39 @@ +// +// MagicViewHostProvider.swift +// +// +// Created by Tristan Warner-Smith on 27/02/2024. +// + +import UIKit + +public protocol MagicViewHostProviding { + func provide() throws -> UIViewController +} + +struct MagicViewHostProvider: MagicViewHostProviding { + public init() {} + + func provide() throws -> UIViewController { + let window = try keyWindow() + // Find topmost view controller from the hierarchy and move webview to it + if var topController = window.rootViewController { + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + return topController + } else { + throw WebViewController.AuthRelayerError.topMostWindowNotFound + } + } +} + +private extension MagicViewHostProvider { + func keyWindow() throws -> UIWindow { + guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow}).first else { + throw WebViewController.AuthRelayerError.topMostWindowNotFound + } + + return window + } +} diff --git a/Sources/MagicSDK/Core/Provider/RpcProvider.swift b/Sources/MagicSDK/Core/Provider/RpcProvider.swift index 8c7b496..8bb7ca5 100644 --- a/Sources/MagicSDK/Core/Provider/RpcProvider.swift +++ b/Sources/MagicSDK/Core/Provider/RpcProvider.swift @@ -24,16 +24,18 @@ public class RpcProvider: NetworkClient, Web3Provider { /// Missing callback case missingPayloadCallback(json: String) } - + + var webViewPresenter: WebViewControllerPresenting { overlay } + let overlay: WebViewController public let urlBuilder: URLBuilder - required init(urlBuilder: URLBuilder) { - self.overlay = WebViewController(url: urlBuilder) + required init(urlBuilder: URLBuilder, viewHostProvider: MagicViewHostProviding) { + self.overlay = WebViewController(url: urlBuilder, viewHostProvider: viewHostProvider) self.urlBuilder = urlBuilder super.init() } - + // MARK: - Sending Requests /// Sends an RPCRequest and parses the result diff --git a/Sources/MagicSDK/Core/Relayer/WebViewController.swift b/Sources/MagicSDK/Core/Relayer/WebViewController.swift index e550b9f..c87f25d 100644 --- a/Sources/MagicSDK/Core/Relayer/WebViewController.swift +++ b/Sources/MagicSDK/Core/Relayer/WebViewController.swift @@ -9,6 +9,11 @@ import WebKit import UIKit +public protocol WebViewControllerPresenting { + func show() throws + func hide() throws +} + /// An instance of the Fortmatc Phantom WebView class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate, UIScrollViewDelegate { @@ -37,17 +42,20 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, /// Queue and callbackss var queue: [String] = [] var messageHandlers: Dictionary = [:] + var viewHostProvider: MagicViewHostProviding typealias MessageHandler = (String) throws -> Void // MARK: - init - init(url: URLBuilder) { + init(url: URLBuilder, viewHostProvider: MagicViewHostProviding) { self.urlBuilder = url + self.viewHostProvider = viewHostProvider super.init(nibName: nil, bundle: nil) } // Required provided by subclass of 'UIViewController' required init?(coder aDecoder: NSCoder) { + self.viewHostProvider = MagicViewHostProvider() super.init(coder: aDecoder) } @@ -60,11 +68,8 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, private func dequeue() throws -> Void { - // Check if UI is appeneded properly to current screen before dequeue - guard let window = UIApplication.shared.keyWindow else { return try attachWebView() } - - if self.view.isDescendant(of: window) { - + // Check if UI is appended properly to current screen before dequeue + if try isAttached() { if !queue.isEmpty && overlayReady && webViewFinishLoading { let message = queue.removeFirst() try self.postMessage(message: message) @@ -92,9 +97,9 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, overlayReady = true try? self.dequeue() } else if payloadStr.contains(InboundMessageType.MAGIC_SHOW_OVERLAY.rawValue) { - try bringWebViewToFront() + try show() } else if payloadStr.contains(InboundMessageType.MAGIC_HIDE_OVERLAY.rawValue) { - try sendSubviewToBack() + try hide() } else if payloadStr.contains(InboundMessageType.MAGIC_HANDLE_EVENT.rawValue) { try handleEvent(payloadStr: payloadStr) } else if payloadStr.contains(InboundMessageType.MAGIC_HANDLE_RESPONSE.rawValue) { @@ -102,7 +107,7 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, } } try self.dequeue() - }catch let error { + } catch let error { print("Magic internal error: \(error.localizedDescription)") } } @@ -163,9 +168,6 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, webView.evaluateJavaScript(execString) } - - - // MARK: - view loading /// loadView will be triggered when addsubview is called. It will create a webview to post messages to auth relayer override func loadView() { @@ -246,30 +248,28 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, // handle external link clicked events func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - // Check for links. - if navigationAction.navigationType == .linkActivated { - // Make sure the URL is set. - guard let url = navigationAction.request.url else { - decisionHandler(.allow) - return - } + // Check for links. + if navigationAction.navigationType == .linkActivated { + // Make sure the URL is set. + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } - // Check for the scheme component. - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if components?.scheme == "http" || components?.scheme == "https" { - // Open the link in the external browser. - UIApplication.shared.open(url) - // Cancel the decisionHandler because we managed the navigationAction. - decisionHandler(.cancel) - } else { - decisionHandler(.allow) - } + // Check for the scheme component. + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + if components?.scheme == "http" || components?.scheme == "https" { + // Open the link in the external browser. + UIApplication.shared.open(url) + // Cancel the decisionHandler because we managed the navigationAction. + decisionHandler(.cancel) } else { decisionHandler(.allow) } + } else { + decisionHandler(.allow) } - - + } // MARK: - View @@ -277,47 +277,44 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, func viewForZooming(in: UIScrollView) -> UIView? { return nil; } +} - private func sendSubviewToBack() throws -> Void { +extension WebViewController: WebViewControllerPresenting { + func show() throws { + let isAlreadyAttached = try isAttached() + let container = try viewHostProvider.provide() + + if !isAlreadyAttached { + try attachWebView() + } - let keyWindow = try getKeyWindow() - keyWindow.sendSubviewToBack(self.view) + container.view.bringSubviewToFront(view) } - private func bringWebViewToFront() throws -> Void { - - let keyWindow = try getKeyWindow() - keyWindow.bringSubviewToFront(self.view) + func hide() throws { + try detachWebView() } +} - private func getKeyWindow() throws -> UIWindow { - - guard let keyWindow = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else { - throw AuthRelayerError.topMostWindowNotFound - } - - return keyWindow +private extension WebViewController { + func isAttached() throws -> Bool { + let viewController = try viewHostProvider.provide() + return view.isDescendant(of: viewController.view) } - private func attachWebView() throws -> Void { - - let keyWindow = try getKeyWindow() - - keyWindow.addSubview(self.view) - keyWindow.sendSubviewToBack(self.view) - - // find topmost view controller from the hierarchy and move webview to it - if var topController = keyWindow.rootViewController { - while let presentedViewController = topController.presentedViewController { - topController = presentedViewController - } - - self.didMove(toParent: topController) - - } else { + func attachWebView() throws { + guard try !isAttached() else { throw AuthRelayerError.webviewAttachedFailed } + let container = try viewHostProvider.provide() + container.view.addSubview(view) + container.view.sendSubviewToBack(view) + self.didMove(toParent: container) } -} - + func detachWebView() throws { + guard try isAttached() else { return } + view.superview?.sendSubviewToBack(view) + view.removeFromSuperview() + } +} From fb9a5f46001dde82ee203f4ff5b88223a3abfe0a Mon Sep 17 00:00:00 2001 From: Tristan Warner-Smith Date: Thu, 29 Feb 2024 10:45:19 +0000 Subject: [PATCH 2/4] Adds cancelLogin function to AuthModule --- Sources/MagicSDK/Modules/Auth/AuthModule.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/MagicSDK/Modules/Auth/AuthModule.swift b/Sources/MagicSDK/Modules/Auth/AuthModule.swift index 0884c18..a8062ab 100644 --- a/Sources/MagicSDK/Modules/Auth/AuthModule.swift +++ b/Sources/MagicSDK/Modules/Auth/AuthModule.swift @@ -52,7 +52,21 @@ public class AuthModule: BaseModule { loginWithEmailOTP(configuration, response: promiseResolver(resolver)) } } - + + public func cancelLogin() { + if #available(iOS 14.0, *) { + AuthModule.logger.warning("cancelLogin: \(BaseWarningLog.MA_Method)") + } else { + print("cancelLogin: \(BaseWarningLog.MA_Method)") + } + + do { + try self.provider.webViewPresenter.hide() + } catch let error { + debugPrint("Failed to dismiss login view due to \(error.localizedDescription)") + } + } + public enum LoginEmailOTPLinkEvent: String { case emailNotDeliverable = "email-not-deliverable" case emailSent = "email-sent" From b64e5ed6737312595a7cdcf0f608910b7f5149db Mon Sep 17 00:00:00 2001 From: Tristan Warner-Smith Date: Mon, 4 Mar 2024 10:47:51 +0000 Subject: [PATCH 3/4] Adds MARK for clarity --- Sources/MagicSDK/Core/Relayer/WebViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/MagicSDK/Core/Relayer/WebViewController.swift b/Sources/MagicSDK/Core/Relayer/WebViewController.swift index c87f25d..2622270 100644 --- a/Sources/MagicSDK/Core/Relayer/WebViewController.swift +++ b/Sources/MagicSDK/Core/Relayer/WebViewController.swift @@ -279,6 +279,8 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, } } +// MARK: - Presentation + extension WebViewController: WebViewControllerPresenting { func show() throws { let isAlreadyAttached = try isAttached() @@ -296,6 +298,8 @@ extension WebViewController: WebViewControllerPresenting { } } +// MARK: - View Hierarchy Helpers + private extension WebViewController { func isAttached() throws -> Bool { let viewController = try viewHostProvider.provide() From df12d9406ca2405144c7fa03d9439fff0647a60a Mon Sep 17 00:00:00 2001 From: Tristan Warner-Smith Date: Sun, 17 Mar 2024 22:01:42 +0000 Subject: [PATCH 4/4] Addresses PR feedback, extracting viewHostProvider to a single convenience initialiser and making hierarchy removal optional --- Sources/MagicSDK/Core/Magic.swift | 19 ++++++++------ .../Core/Relayer/WebViewController.swift | 25 +++++++++++++------ .../MagicSDK/Modules/Auth/AuthModule.swift | 4 +-- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/Sources/MagicSDK/Core/Magic.swift b/Sources/MagicSDK/Core/Magic.swift index 2a6cca5..3b69578 100644 --- a/Sources/MagicSDK/Core/Magic.swift +++ b/Sources/MagicSDK/Core/Magic.swift @@ -34,21 +34,24 @@ public class Magic: NSObject { /// - ethNetwork: Etherum Network setting (ie. mainnet or goerli) /// - customNode: A custom RPC node /// - viewHostProvider: An optional `UIViewController` provider for login views to embed within - public convenience init(apiKey: String, ethNetwork: EthNetwork, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: ethNetwork, locale: locale), viewHostProvider: viewHostProvider) + public convenience init(apiKey: String, ethNetwork: EthNetwork, locale: String = Locale.current.identifier) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: ethNetwork, locale: locale)) } - public convenience init(apiKey: String, customNode: CustomNodeConfiguration, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, customNode: customNode, locale: locale), viewHostProvider: viewHostProvider) + public convenience init(apiKey: String, customNode: CustomNodeConfiguration, locale: String = Locale.current.identifier) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, customNode: customNode, locale: locale)) } - public convenience init(apiKey: String, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding? = nil) { - self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: EthNetwork.mainnet, locale: locale), viewHostProvider: viewHostProvider) + public convenience init(apiKey: String, locale: String = Locale.current.identifier) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: EthNetwork.mainnet, locale: locale)) + } + + public convenience init(apiKey: String, locale: String = Locale.current.identifier, viewHostProvider: MagicViewHostProviding) { + self.init(urlBuilder: URLBuilder(apiKey: apiKey, locale: locale), viewHostProvider: viewHostProvider) } /// Core constructor - private init(urlBuilder: URLBuilder, viewHostProvider: MagicViewHostProviding?) { - let viewHostProvider = viewHostProvider ?? MagicViewHostProvider() + private init(urlBuilder: URLBuilder, viewHostProvider: MagicViewHostProviding = MagicViewHostProvider()) { self.rpcProvider = RpcProvider(urlBuilder: urlBuilder, viewHostProvider: viewHostProvider) self.user = UserModule(rpcProvider: self.rpcProvider) diff --git a/Sources/MagicSDK/Core/Relayer/WebViewController.swift b/Sources/MagicSDK/Core/Relayer/WebViewController.swift index 2622270..5853635 100644 --- a/Sources/MagicSDK/Core/Relayer/WebViewController.swift +++ b/Sources/MagicSDK/Core/Relayer/WebViewController.swift @@ -11,7 +11,7 @@ import UIKit public protocol WebViewControllerPresenting { func show() throws - func hide() throws + func hide(remove: Bool) throws } /// An instance of the Fortmatc Phantom WebView @@ -284,17 +284,19 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, extension WebViewController: WebViewControllerPresenting { func show() throws { let isAlreadyAttached = try isAttached() - let container = try viewHostProvider.provide() - if !isAlreadyAttached { try attachWebView() } - container.view.bringSubviewToFront(view) + try bringToFront() } - func hide() throws { - try detachWebView() + func hide(remove: Bool = false) throws { + if remove { + try detachWebView() + } else { + try sendToBack() + } } } @@ -318,7 +320,16 @@ private extension WebViewController { func detachWebView() throws { guard try isAttached() else { return } - view.superview?.sendSubviewToBack(view) + try sendToBack() view.removeFromSuperview() } + + func bringToFront() throws { + view.superview?.bringSubviewToFront(view) + } + + func sendToBack() throws { + guard try isAttached() else { return } + view.superview?.sendSubviewToBack(view) + } } diff --git a/Sources/MagicSDK/Modules/Auth/AuthModule.swift b/Sources/MagicSDK/Modules/Auth/AuthModule.swift index a8062ab..7e935f9 100644 --- a/Sources/MagicSDK/Modules/Auth/AuthModule.swift +++ b/Sources/MagicSDK/Modules/Auth/AuthModule.swift @@ -53,7 +53,7 @@ public class AuthModule: BaseModule { } } - public func cancelLogin() { + public func cancelLogin(remove: Bool = false) { if #available(iOS 14.0, *) { AuthModule.logger.warning("cancelLogin: \(BaseWarningLog.MA_Method)") } else { @@ -61,7 +61,7 @@ public class AuthModule: BaseModule { } do { - try self.provider.webViewPresenter.hide() + try self.provider.webViewPresenter.hide(remove: remove) } catch let error { debugPrint("Failed to dismiss login view due to \(error.localizedDescription)") }