Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions Sources/MagicSDK/Core/Magic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -32,7 +32,8 @@ 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
/// - 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) {
self.init(urlBuilder: URLBuilder(apiKey: apiKey, network: ethNetwork, locale: locale))
}
Expand All @@ -45,15 +46,19 @@ public class Magic: NSObject {
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) {
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 = 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()
}
}

Expand Down
39 changes: 39 additions & 0 deletions Sources/MagicSDK/Core/Provider/MagicViewHostProvider.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 6 additions & 4 deletions Sources/MagicSDK/Core/Provider/RpcProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 72 additions & 60 deletions Sources/MagicSDK/Core/Relayer/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import WebKit
import UIKit

public protocol WebViewControllerPresenting {
func show() throws
func hide(remove: Bool) throws
}

/// An instance of the Fortmatc Phantom WebView
class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate, UIScrollViewDelegate {

Expand Down Expand Up @@ -37,17 +42,20 @@ class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler,
/// Queue and callbackss
var queue: [String] = []
var messageHandlers: Dictionary<Int, MessageHandler> = [:]
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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -92,17 +97,17 @@ 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) {
try handleResponse(payloadStr: payloadStr)
}
}
try self.dequeue()
}catch let error {
} catch let error {
print("Magic internal error: \(error.localizedDescription)")
}
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -246,78 +248,88 @@ 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

/// Disable zooming for webview
func viewForZooming(in: UIScrollView) -> UIView? {
return nil;
}
}

private func sendSubviewToBack() throws -> Void {

let keyWindow = try getKeyWindow()
keyWindow.sendSubviewToBack(self.view)
}
// MARK: - Presentation

private func bringWebViewToFront() throws -> Void {
extension WebViewController: WebViewControllerPresenting {
func show() throws {
let isAlreadyAttached = try isAttached()
if !isAlreadyAttached {
try attachWebView()
}

let keyWindow = try getKeyWindow()
keyWindow.bringSubviewToFront(self.view)
try bringToFront()
}

private func getKeyWindow() throws -> UIWindow {

guard let keyWindow = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else {
throw AuthRelayerError.topMostWindowNotFound
func hide(remove: Bool = false) throws {
if remove {
try detachWebView()
} else {
try sendToBack()
}

return keyWindow
}
}

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
}
// MARK: - View Hierarchy Helpers

self.didMove(toParent: topController)
private extension WebViewController {
func isAttached() throws -> Bool {
let viewController = try viewHostProvider.provide()
return view.isDescendant(of: viewController.view)
}

} 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 }
try sendToBack()
view.removeFromSuperview()
}

func bringToFront() throws {
view.superview?.bringSubviewToFront(view)
}

func sendToBack() throws {
guard try isAttached() else { return }
view.superview?.sendSubviewToBack(view)
}
}
16 changes: 15 additions & 1 deletion Sources/MagicSDK/Modules/Auth/AuthModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,21 @@ public class AuthModule: BaseModule {
loginWithEmailOTP(configuration, response: promiseResolver(resolver))
}
}


public func cancelLogin(remove: Bool = false) {
if #available(iOS 14.0, *) {
AuthModule.logger.warning("cancelLogin: \(BaseWarningLog.MA_Method)")
} else {
print("cancelLogin: \(BaseWarningLog.MA_Method)")
}

do {
try self.provider.webViewPresenter.hide(remove: remove)
} 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"
Expand Down