From c6c444aa2dcda8eea18d8edfeb6754fddd8884f1 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 31 Jan 2024 11:53:31 +0100 Subject: [PATCH 1/6] Prepare models for magic --- .../Web3Modal/Components/AccountButton.swift | 5 +- Sources/Web3Modal/Core/MagicMessage.swift | 205 ++++++++++++++++++ Sources/Web3Modal/Store.swift | 17 ++ 3 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 Sources/Web3Modal/Core/MagicMessage.swift diff --git a/Sources/Web3Modal/Components/AccountButton.swift b/Sources/Web3Modal/Components/AccountButton.swift index 83f8098..d06fd46 100644 --- a/Sources/Web3Modal/Components/AccountButton.swift +++ b/Sources/Web3Modal/Components/AccountButton.swift @@ -203,10 +203,7 @@ public struct AccountButtonPreviewView: View { let store = Store() store.balance = balance store.session = .stub - store.account = W3MAccount( - address: Session.stub.accounts.first!.address, - chain: Session.stub.accounts.first!.blockchain - ) + store.account = .stub return store } diff --git a/Sources/Web3Modal/Core/MagicMessage.swift b/Sources/Web3Modal/Core/MagicMessage.swift new file mode 100644 index 0000000..a06c014 --- /dev/null +++ b/Sources/Web3Modal/Core/MagicMessage.swift @@ -0,0 +1,205 @@ +import Foundation + +class MagicMessage { + var type: String + var payload: Any? + var rt: String? + var jwt: String? + var action: String? + + init(type: String, payload: Any? = nil, rt: String? = nil, jwt: String? = nil, action: String? = nil) { + self.type = type + self.payload = payload + self.rt = rt + self.jwt = jwt + self.action = action + } + + func toJson() -> [String: Any] { + var params: [String: Any] = ["type": type] + if let payload = payload { + params["payload"] = payload + } + if let rt = rt, !rt.isEmpty { + params["rt"] = rt + } + if let jwt = jwt, !jwt.isEmpty { + params["jwt"] = jwt + } + if let action = action, !action.isEmpty { + params["action"] = action + } + return params + } + + var description: String { return "{type: \"\(type)\"}" } + + // @w3m-frame events + var syncThemeSuccess: Bool { type == "@w3m-frame/SYNC_THEME_SUCCESS" } + var syncDataSuccess: Bool { type == "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" } + var connectEmailSuccess: Bool { type == "@w3m-frame/CONNECT_EMAIL_SUCCESS" } + var connectEmailError: Bool { type == "@w3m-frame/CONNECT_EMAIL_ERROR" } + var isConnectSuccess: Bool { type == "@w3m-frame/IS_CONNECTED_SUCCESS" } + var isConnectError: Bool { type == "@w3m-frame/IS_CONNECTED_ERROR" } + var connectOtpSuccess: Bool { type == "@w3m-frame/CONNECT_OTP_SUCCESS" } + var connectOtpError: Bool { type == "@w3m-frame/CONNECT_OTP_ERROR" } + var getUserSuccess: Bool { type == "@w3m-frame/GET_USER_SUCCESS" } + var getUserError: Bool { type == "@w3m-frame/GET_USER_ERROR" } + var sessionUpdate: Bool { type == "@w3m-frame/SESSION_UPDATE" } + var switchNetworkSuccess: Bool { type == "@w3m-frame/SWITCH_NETWORK_SUCCESS" } + var switchNetworkError: Bool { type == "@w3m-frame/SWITCH_NETWORK_ERROR" } + var rpcRequestSuccess: Bool { type == "@w3m-frame/RPC_REQUEST_SUCCESS" } + var rpcRequestError: Bool { type == "@w3m-frame/RPC_REQUEST_ERROR" } +} + +class IsConnected: MagicMessage { + init() { + super.init(type: "@w3m-app/IS_CONNECTED") + } +} + +class SwitchNetwork: MagicMessage { + let chainId: String + + init(chainId: String) { + self.chainId = chainId + super.init(type: "@w3m-app/SWITCH_NETWORK") + } + + override var description: String { + "{type:'\(type)',payload:{chainId:\(chainId)}}" + } +} + +class ConnectEmail: MagicMessage { + let email: String + + init(email: String) { + self.email = email + super.init(type: "@w3m-app/CONNECT_EMAIL") + } + + override var description: String { + "{type:'\(type)',payload:{email:'\(email)'}}" + } +} + +class ConnectDevice: MagicMessage { + init() { + super.init(type: "@w3m-app/CONNECT_DEVICE") + } +} + +class ConnectOtp: MagicMessage { + let otp: String + + init(otp: String) { + self.otp = otp + super.init(type: "@w3m-app/CONNECT_OTP") + } + + override var description: String { + "{type:'\(type)',payload:{otp:'\(otp)'}}" + } +} + +class GetUser: MagicMessage { + + let chainId: String? + + init(chainId: String) { + self.chainId = chainId + super.init(type: "@w3m-app/GET_USER") + } + + override var description: String { + if let chainId { + return "{type:'\(type)',payload:{chainId:\(chainId)}}" + } else { + return "{type:'\(type)'}" + } + } +} + +class SignOut: MagicMessage { + init() { + super.init(type: "@w3m-app/SIGN_OUT") + } +} + +class GetChainId: MagicMessage { + init() { + super.init(type: "@w3m-app/GET_CHAIN_ID") + } +} + +class RpcRequest: MagicMessage { + let method: String + let params: [Any] + + init(method: String, params: [Any]) { + self.method = method + self.params = params + super.init(type: "@w3m-app/RPC_REQUEST") + } + + override var description: String { + let m = "method:'\(method)'" + let p = params.map { "'\($0)'" }.joined(separator: ",") + return "{type:'\(type)',payload:{\(m),params:[\(p)]}}" + } +} + +// readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; +class UpdateEmail: MagicMessage { + init() { + super.init(type: "@w3m-app/UPDATE_EMAIL") + } +} + +class SyncTheme: MagicMessage { + var mode: String + + init(mode: String = "light") { + self.mode = mode + super.init(type: "@w3m-app/SYNC_THEME") + } + + override var description: String { + let tm = "themeMode:'\(mode)'" + return "{type:'\(type)',payload:{\(tm)}}" + } +} + +struct PairingMetadata { + let name: String + let description: String + let url: String + let icons: [String] +} + +class SyncAppData: MagicMessage { + let metadata: PairingMetadata + let sdkVersion: String + let projectId: String + + init(metadata: PairingMetadata, sdkVersion: String, projectId: String) { + self.metadata = metadata + self.sdkVersion = sdkVersion + self.projectId = projectId + super.init(type: "@w3m-app/SYNC_DAPP_DATA") + } + + override var description: String { + let v = "verified: true" + let p1 = "projectId:'\(projectId)'" + let p2 = "sdkVersion:'\(sdkVersion)'" + let m1 = "name:'\(metadata.name)'" + let m2 = "description:'\(metadata.description)'" + let m3 = "url:'\(metadata.url)'" + let m4 = "icons:[\"\(metadata.icons.first ?? "")\"]" + let p3 = "metadata:{\(m1),\(m2),\(m3),\(m4)}" + let p = "payload:{\(v),\(p1),\(p2),\(p3)}" + return "{type:'\(type)',\(p)}" + } +} diff --git a/Sources/Web3Modal/Store.swift b/Sources/Web3Modal/Store.swift index 22e4f7d..67c279f 100644 --- a/Sources/Web3Modal/Store.swift +++ b/Sources/Web3Modal/Store.swift @@ -66,6 +66,23 @@ class Store: ObservableObject { @Published var chainImages: [String: UIImage] = [:] @Published var toast: Toast? = nil + + + // Magic specific + var magicSession: MagicSession? +} + +struct MagicSession: Codable { + let pk: String + let jwt: String + let rt: String + let userData: MagicUserData + + struct MagicUserData: Codable { + let email: String + let address: String + let chainId: Int + } } struct W3MAccount: Codable { From 3a94a36097643ae32b4c161c6280e3ec563e2608 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 31 Jan 2024 17:29:32 +0100 Subject: [PATCH 2/6] Setup secure site website with message handling/posting --- Sources/Web3Modal/Core/MagicMessage.swift | 18 +- Sources/Web3Modal/Core/MagicService.swift | 187 ++++++++++++++++++ Sources/Web3Modal/Core/Web3Modal.swift | 4 + .../ConnectWallet/ConnectWalletView.swift | 28 +++ 4 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 Sources/Web3Modal/Core/MagicService.swift diff --git a/Sources/Web3Modal/Core/MagicMessage.swift b/Sources/Web3Modal/Core/MagicMessage.swift index a06c014..0c10f5d 100644 --- a/Sources/Web3Modal/Core/MagicMessage.swift +++ b/Sources/Web3Modal/Core/MagicMessage.swift @@ -32,7 +32,7 @@ class MagicMessage { return params } - var description: String { return "{type: \"\(type)\"}" } + var toString: String { return "{type: \"\(type)\"}" } // @w3m-frame events var syncThemeSuccess: Bool { type == "@w3m-frame/SYNC_THEME_SUCCESS" } @@ -66,7 +66,7 @@ class SwitchNetwork: MagicMessage { super.init(type: "@w3m-app/SWITCH_NETWORK") } - override var description: String { + override var toString: String { "{type:'\(type)',payload:{chainId:\(chainId)}}" } } @@ -79,7 +79,7 @@ class ConnectEmail: MagicMessage { super.init(type: "@w3m-app/CONNECT_EMAIL") } - override var description: String { + override var toString: String { "{type:'\(type)',payload:{email:'\(email)'}}" } } @@ -98,7 +98,7 @@ class ConnectOtp: MagicMessage { super.init(type: "@w3m-app/CONNECT_OTP") } - override var description: String { + override var toString: String { "{type:'\(type)',payload:{otp:'\(otp)'}}" } } @@ -107,12 +107,12 @@ class GetUser: MagicMessage { let chainId: String? - init(chainId: String) { + init(chainId: String?) { self.chainId = chainId super.init(type: "@w3m-app/GET_USER") } - override var description: String { + override var toString: String { if let chainId { return "{type:'\(type)',payload:{chainId:\(chainId)}}" } else { @@ -143,7 +143,7 @@ class RpcRequest: MagicMessage { super.init(type: "@w3m-app/RPC_REQUEST") } - override var description: String { + override var toString: String { let m = "method:'\(method)'" let p = params.map { "'\($0)'" }.joined(separator: ",") return "{type:'\(type)',payload:{\(m),params:[\(p)]}}" @@ -165,7 +165,7 @@ class SyncTheme: MagicMessage { super.init(type: "@w3m-app/SYNC_THEME") } - override var description: String { + override var toString: String { let tm = "themeMode:'\(mode)'" return "{type:'\(type)',payload:{\(tm)}}" } @@ -190,7 +190,7 @@ class SyncAppData: MagicMessage { super.init(type: "@w3m-app/SYNC_DAPP_DATA") } - override var description: String { + override var toString: String { let v = "verified: true" let p1 = "projectId:'\(projectId)'" let p2 = "sdkVersion:'\(sdkVersion)'" diff --git a/Sources/Web3Modal/Core/MagicService.swift b/Sources/Web3Modal/Core/MagicService.swift new file mode 100644 index 0000000..abe256e --- /dev/null +++ b/Sources/Web3Modal/Core/MagicService.swift @@ -0,0 +1,187 @@ +import Foundation +import WebKit + +enum Web3ModalTheme: String { + case dark + case light +} + +class MagicService { + private let injectScript = """ + window.addEventListener('message', ({ data }) => { + window.webkit.messageHandlers.nativeProcess.postMessage(JSON.stringify(data)) + }) + + const sendMessage = async (message) => { + postMessage(message, '*') + } + """ + + private let url = "https://secure.walletconnect.com" + private let projectId: String = Web3Modal.config.projectId +// private let metadata: PairingMetadata + + private let messageHandler: MessageHandler + private let navigationDelegate: NavigationDelegate + private let webview: WKWebView + private let contentController: WKUserContentController + + private var attachedToViewHierarchy = false + + init() { + self.messageHandler = MessageHandler() + self.navigationDelegate = NavigationDelegate() + + self.contentController = WKUserContentController() + contentController.add(messageHandler, name: "nativeProcess") + contentController.addUserScript( + WKUserScript( + source: injectScript, + injectionTime: .atDocumentEnd, + forMainFrameOnly: false + ) + ) + + let webConfiguration = WKWebViewConfiguration() + webConfiguration.websiteDataStore = WKWebsiteDataStore.default() + webConfiguration.userContentController = contentController + if #available(iOS 14.0, *) { + webConfiguration.defaultWebpagePreferences.allowsContentJavaScript = true + } else { + // Fallback on earlier versions + } + + self.webview = WKWebView( + frame: CGRect(x: 0, y: 0, width: 400, height: 400), + configuration: webConfiguration + ) + + webview.navigationDelegate = navigationDelegate + + if #available(iOS 16.4, *) { + webview.isInspectable = true + } else { + // Fallback on earlier versions + } + + let url = URL(string: "\(url)/sdk?projectId=\(projectId)")! + + webview.load( + URLRequest(url: url) + ) + } + + func attachToViewHierarchy() { + guard let window = UIApplication.keyWindow else { return } + window.addSubview(webview) + window.sendSubviewToBack(webview) + + attachedToViewHierarchy = true + } + + func connectEmail(email: String) async { + let message = ConnectEmail(email: email).toString + await runJavaScript("sendMessage(\(message))") + } + + func connectDevice() async { + let message = ConnectDevice().toString + await runJavaScript("sendMessage(\(message))") + } + + func connectOtp(otp: String) async { + // Assuming waitConfirmation is a property you're using to track state + // waitConfirmation.value = true + let message = ConnectOtp(otp: otp).toString + await runJavaScript("sendMessage(\(message))") + } + + func isConnected() async { + let message = IsConnected().toString + await runJavaScript("sendMessage(\(message))") + } + + func getChainId() async { + let message = GetChainId().toString + await runJavaScript("sendMessage(\(message))") + } + + // Example of a commented-out function, updateEmail, for reference + // func updateEmail(email: String) async { + // await runJavaScript("provider.updateEmail('\(email)')") + // } + + func syncTheme(theme: Web3ModalTheme?) async { + guard let mode = theme?.rawValue else { return } + let message = SyncTheme(mode: mode) + await runJavaScript("sendMessage(\(message))") + } + + func syncDappData(metadata: PairingMetadata, sdkVersion: String, projectId: String) async { + let message = SyncAppData(metadata: metadata, sdkVersion: sdkVersion, projectId: projectId).toString + await runJavaScript("sendMessage(\(message))") + } + + func getUser(chainId: String?) async { + let message = GetUser(chainId: chainId).toString + await runJavaScript("sendMessage(\(message))") + } + + func switchNetwork(chainId: String) async { + let message = SwitchNetwork(chainId: chainId).toString + await runJavaScript("sendMessage(\(message))") + } + + func disconnect() async { + let message = "SignOut()" + await runJavaScript("sendMessage(\(message))") + } + + func request(parameters: [String: Any]) async { + guard let method = parameters["method"] as? String, + let params = parameters["params"] as? [Any] else { return } + let message = "RpcRequest(method: \(method), params: \(params))" + await runJavaScript("sendMessage(\(message))") + // Handle onApproveTransaction if needed + } + + @MainActor + private func runJavaScript(_ script: String) async { + if !attachedToViewHierarchy { + attachToViewHierarchy() + } + + do { + let result = try await webview.evaluateJavaScript(""" + setTimeout(() => { + \(script) + }, 100) + """) + } catch { + print("JavaScript execution error: \(error)") + } + } +} + +class MessageHandler: NSObject, WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + print(message.body) + } +} + +class NavigationDelegate: NSObject, WKNavigationDelegate { + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {} +} + +extension UIApplication { + static var keyWindow: UIWindow? { + let allScenes = UIApplication.shared.connectedScenes + for scene in allScenes { + guard let windowScene = scene as? UIWindowScene else { continue } + for window in windowScene.windows where window.isKeyWindow { + return window + } + } + return nil + } +} diff --git a/Sources/Web3Modal/Core/Web3Modal.swift b/Sources/Web3Modal/Core/Web3Modal.swift index a078b42..2151770 100644 --- a/Sources/Web3Modal/Core/Web3Modal.swift +++ b/Sources/Web3Modal/Core/Web3Modal.swift @@ -80,6 +80,8 @@ public class Web3Modal { private(set) static var config: Config! private(set) static var viewModel: Web3ModalViewModel! + + private(set) static var magicService: MagicService? private init() {} @@ -125,6 +127,8 @@ public class Web3Modal { w3mApiInteractor: w3mApiInteractor ) + Web3Modal.magicService = MagicService() + Web3Modal.viewModel = Web3ModalViewModel( router: router, store: store, diff --git a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift index 6d5c15f..015e70c 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift @@ -6,6 +6,8 @@ struct ConnectWalletView: View { let displayWCConnection = false + @State var email: String = "radek@walletconnect.com" + var wallets: [Wallet] { var recentWallets = store.recentWallets @@ -29,6 +31,10 @@ struct ConnectWalletView: View { var body: some View { VStack { + emailInput() + + Divider() + wcConnection() featuredWallets() @@ -49,6 +55,28 @@ struct ConnectWalletView: View { .padding(.bottom) } + @ViewBuilder + private func emailInput() -> some View { + TextField("Enter your email", text: $email) + .font(.body) + .padding(.bottom, Spacing.s) + .backport.overlay { + RoundedRectangle(cornerRadius: Radius.xxs) + .stroke(Color.Overgray010, lineWidth: 1) + } + .backport.overlay(alignment: .trailing) { + Button(action: { + Task { + await Web3Modal.magicService?.connectEmail(email: email) + } + }, label: { + Image(systemName: "chevron.right.circle.fill") + }) + .disabled(email.count < 5) // TODO: Regex prolly? + } + + } + @ViewBuilder private func featuredWallets() -> some View { ForEach(wallets, id: \.self) { wallet in From 923d06110ff095994f3a65561713cb544690852b Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 31 Jan 2024 18:33:53 +0100 Subject: [PATCH 3/6] Add otp view; Message decoding (wip) --- Sources/Web3Modal/Core/MagicMessage.swift | 170 ++++++++++++------ Sources/Web3Modal/Core/MagicService.swift | 10 +- Sources/Web3Modal/Router.swift | 1 + .../Screens/ConnectWallet/SwiftUIView.swift | 115 ++++++++++++ Sources/Web3Modal/Sheets/Web3ModalView.swift | 4 + 5 files changed, 249 insertions(+), 51 deletions(-) create mode 100644 Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift diff --git a/Sources/Web3Modal/Core/MagicMessage.swift b/Sources/Web3Modal/Core/MagicMessage.swift index 0c10f5d..5a7030c 100644 --- a/Sources/Web3Modal/Core/MagicMessage.swift +++ b/Sources/Web3Modal/Core/MagicMessage.swift @@ -1,60 +1,85 @@ import Foundation -class MagicMessage { - var type: String - var payload: Any? + +protocol MagicMessageType: Codable { + var rawValue: String { get } +} + +enum MagicMessageResponseType: String, MagicMessageType, Codable { + case syncThemeSuccess = "@w3m-frame/SYNC_THEME_SUCCESS" + case syncDataSuccess = "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" + case connectEmailSuccess = "@w3m-frame/CONNECT_EMAIL_SUCCESS" + case connectEmailError = "@w3m-frame/CONNECT_EMAIL_ERROR" + case isConnectSuccess = "@w3m-frame/IS_CONNECTED_SUCCESS" + case isConnectError = "@w3m-frame/IS_CONNECTED_ERROR" + case connectOtpSuccess = "@w3m-frame/CONNECT_OTP_SUCCESS" + case connectOtpError = "@w3m-frame/CONNECT_OTP_ERROR" + case getUserSuccess = "@w3m-frame/GET_USER_SUCCESS" + case getUserError = "@w3m-frame/GET_USER_ERROR" + case sessionUpdate = "@w3m-frame/SESSION_UPDATE" + case switchNetworkSuccess = "@w3m-frame/SWITCH_NETWORK_SUCCESS" + case switchNetworkError = "@w3m-frame/SWITCH_NETWORK_ERROR" + case rpcRequestSuccess = "@w3m-frame/RPC_REQUEST_SUCCESS" + case rpcRequestError = "@w3m-frame/RPC_REQUEST_ERROR" +} + +enum MagicMessageRequestType: String, MagicMessageType, Codable { + case syncTheme = "@w3m-app/SYNC_THEME" + case syncData = "@w3m-app/SYNC_DAPP_DATA" + case connectEmail = "@w3m-app/CONNECT_EMAIL" + case connectDevice = "@w3m-app/CONNECT_DEVICE" + case isConnected = "@w3m-app/IS_CONNECTED" + case connectOtp = "@w3m-app/CONNECT_OTP" + case switchNetwork = "@w3m-app/SWITCH_NETWORK" + case rpcRequest = "@w3m-app/RPC_REQUEST" + case signOut = "@w3m-app/SIGN_OUT" + case getUser = "@w3m-app/GET_USER" + case getChainId = "@w3m-app/GET_CHAIN_ID" + case updateEmail = "@w3m-app/UPDATE_EMAIL" +} + +class MagicMessage: Decodable { + var type: MagicMessageType + var payload: AnyCodable? var rt: String? var jwt: String? var action: String? + + enum CodingKeys: CodingKey { + case type + case payload + case rt + case jwt + case action + } - init(type: String, payload: Any? = nil, rt: String? = nil, jwt: String? = nil, action: String? = nil) { + init(type: MagicMessageType, payload: AnyCodable? = nil, rt: String? = nil, jwt: String? = nil, action: String? = nil) { self.type = type self.payload = payload self.rt = rt self.jwt = jwt self.action = action } - - func toJson() -> [String: Any] { - var params: [String: Any] = ["type": type] - if let payload = payload { - params["payload"] = payload - } - if let rt = rt, !rt.isEmpty { - params["rt"] = rt - } - if let jwt = jwt, !jwt.isEmpty { - params["jwt"] = jwt - } - if let action = action, !action.isEmpty { - params["action"] = action - } - return params + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(MagicMessageResponseType.self, forKey: .type) + self.payload = try container.decodeIfPresent(AnyCodable.self, forKey: .payload) + self.rt = try container.decodeIfPresent(String.self, forKey: .rt) + self.jwt = try container.decodeIfPresent(String.self, forKey: .jwt) + self.action = try container.decodeIfPresent(String.self, forKey: .action) } var toString: String { return "{type: \"\(type)\"}" } - - // @w3m-frame events - var syncThemeSuccess: Bool { type == "@w3m-frame/SYNC_THEME_SUCCESS" } - var syncDataSuccess: Bool { type == "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" } - var connectEmailSuccess: Bool { type == "@w3m-frame/CONNECT_EMAIL_SUCCESS" } - var connectEmailError: Bool { type == "@w3m-frame/CONNECT_EMAIL_ERROR" } - var isConnectSuccess: Bool { type == "@w3m-frame/IS_CONNECTED_SUCCESS" } - var isConnectError: Bool { type == "@w3m-frame/IS_CONNECTED_ERROR" } - var connectOtpSuccess: Bool { type == "@w3m-frame/CONNECT_OTP_SUCCESS" } - var connectOtpError: Bool { type == "@w3m-frame/CONNECT_OTP_ERROR" } - var getUserSuccess: Bool { type == "@w3m-frame/GET_USER_SUCCESS" } - var getUserError: Bool { type == "@w3m-frame/GET_USER_ERROR" } - var sessionUpdate: Bool { type == "@w3m-frame/SESSION_UPDATE" } - var switchNetworkSuccess: Bool { type == "@w3m-frame/SWITCH_NETWORK_SUCCESS" } - var switchNetworkError: Bool { type == "@w3m-frame/SWITCH_NETWORK_ERROR" } - var rpcRequestSuccess: Bool { type == "@w3m-frame/RPC_REQUEST_SUCCESS" } - var rpcRequestError: Bool { type == "@w3m-frame/RPC_REQUEST_ERROR" } } class IsConnected: MagicMessage { init() { - super.init(type: "@w3m-app/IS_CONNECTED") + super.init(type: MagicMessageRequestType.isConnected) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } } @@ -63,11 +88,16 @@ class SwitchNetwork: MagicMessage { init(chainId: String) { self.chainId = chainId - super.init(type: "@w3m-app/SWITCH_NETWORK") + super.init(type: MagicMessageRequestType.switchNetwork) } + required init(from decoder: Decoder) throws { + try super.init(from: decoder) + let container = try decoder.container(keyedBy: CodingKeys.self) + } + override var toString: String { - "{type:'\(type)',payload:{chainId:\(chainId)}}" + "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" } } @@ -76,7 +106,11 @@ class ConnectEmail: MagicMessage { init(email: String) { self.email = email - super.init(type: "@w3m-app/CONNECT_EMAIL") + super.init(type: MagicMessageRequestType.connectEmail) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } override var toString: String { @@ -86,7 +120,11 @@ class ConnectEmail: MagicMessage { class ConnectDevice: MagicMessage { init() { - super.init(type: "@w3m-app/CONNECT_DEVICE") + super.init(type: MagicMessageRequestType.connectDevice) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } } @@ -95,7 +133,11 @@ class ConnectOtp: MagicMessage { init(otp: String) { self.otp = otp - super.init(type: "@w3m-app/CONNECT_OTP") + super.init(type: MagicMessageRequestType.connectOtp) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } override var toString: String { @@ -109,7 +151,11 @@ class GetUser: MagicMessage { init(chainId: String?) { self.chainId = chainId - super.init(type: "@w3m-app/GET_USER") + super.init(type: MagicMessageRequestType.getUser) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } override var toString: String { @@ -123,13 +169,21 @@ class GetUser: MagicMessage { class SignOut: MagicMessage { init() { - super.init(type: "@w3m-app/SIGN_OUT") + super.init(type: MagicMessageRequestType.signOut) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } } class GetChainId: MagicMessage { init() { - super.init(type: "@w3m-app/GET_CHAIN_ID") + super.init(type: MagicMessageRequestType.getChainId) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } } @@ -140,7 +194,11 @@ class RpcRequest: MagicMessage { init(method: String, params: [Any]) { self.method = method self.params = params - super.init(type: "@w3m-app/RPC_REQUEST") + super.init(type: MagicMessageRequestType.rpcRequest) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } override var toString: String { @@ -153,7 +211,11 @@ class RpcRequest: MagicMessage { // readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; class UpdateEmail: MagicMessage { init() { - super.init(type: "@w3m-app/UPDATE_EMAIL") + super.init(type: MagicMessageRequestType.updateEmail) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } } @@ -162,9 +224,13 @@ class SyncTheme: MagicMessage { init(mode: String = "light") { self.mode = mode - super.init(type: "@w3m-app/SYNC_THEME") + super.init(type: MagicMessageRequestType.syncTheme) } + required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + override var toString: String { let tm = "themeMode:'\(mode)'" return "{type:'\(type)',payload:{\(tm)}}" @@ -187,7 +253,11 @@ class SyncAppData: MagicMessage { self.metadata = metadata self.sdkVersion = sdkVersion self.projectId = projectId - super.init(type: "@w3m-app/SYNC_DAPP_DATA") + super.init(type: MagicMessageRequestType.syncData) + } + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) } override var toString: String { diff --git a/Sources/Web3Modal/Core/MagicService.swift b/Sources/Web3Modal/Core/MagicService.swift index abe256e..2c63266 100644 --- a/Sources/Web3Modal/Core/MagicService.swift +++ b/Sources/Web3Modal/Core/MagicService.swift @@ -165,7 +165,15 @@ class MagicService { class MessageHandler: NSObject, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - print(message.body) + guard let bodyString = message.body as? String, + let data = bodyString.data(using: .utf8) else { return } + + do { + let jsonMessage = try JSONDecoder().decode(MagicMessage.self, from: data) + // Handle the decoded message + } catch { + print("Error decoding message: \(error)") + } } } diff --git a/Sources/Web3Modal/Router.swift b/Sources/Web3Modal/Router.swift index ef021b8..a734b42 100644 --- a/Sources/Web3Modal/Router.swift +++ b/Sources/Web3Modal/Router.swift @@ -48,6 +48,7 @@ class Router: ObservableObject { enum ConnectingSubpage: SubPage { case connectWallet + case otp case qr case allWallets case whatIsAWallet diff --git a/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift b/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift new file mode 100644 index 0000000..a31a220 --- /dev/null +++ b/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift @@ -0,0 +1,115 @@ +import SwiftUI + +struct EnterOTPView: View { + var body: some View { + if #available(iOS 15.0, *) { + OtpView( + activeIndicatorColor: .blue, + inactiveIndicatorColor: .gray, + length: 6, + doSomething: { otp in + print(otp) + } + ) + } else { + EmptyView() + } + } +} + +#if DEBUG +struct EnterOTPView_Previews: PreviewProvider { + static var previews: some View { + EnterOTPView() + } +} +#endif + + +@available(iOS 15.0, *) +public struct OtpView:View { + + private var activeIndicatorColor: Color + private var inactiveIndicatorColor: Color + private let doSomething: (String) -> Void + private let length: Int + + @State private var otpText = "" + @FocusState private var isKeyboardShowing: Bool + + public init( + activeIndicatorColor: Color, + inactiveIndicatorColor: Color, + length: Int, + doSomething: @escaping (String) -> Void + ) { + self.activeIndicatorColor = activeIndicatorColor + self.inactiveIndicatorColor = inactiveIndicatorColor + self.length = length + self.doSomething = doSomething + } + public var body: some View { + HStack(spacing: 0){ + ForEach(0...length-1, id: \.self) { index in + OTPTextBox(index) + } + }.background(content: { + TextField("", text: $otpText.limit(length)) + .keyboardType(.numberPad) + .textContentType(.oneTimeCode) + .frame(width: 1, height: 1) + .opacity(0.001) + .blendMode(.screen) + .focused($isKeyboardShowing) + .onChange(of: otpText) { newValue in + if newValue.count == length { + doSomething(newValue) + } + } + .onAppear { + DispatchQueue.main.async { + isKeyboardShowing = true + } + } + }) + .contentShape(Rectangle()) + .onTapGesture { + isKeyboardShowing = true + } + } + + @ViewBuilder + func OTPTextBox(_ index: Int) -> some View { + ZStack{ + if otpText.count > index { + let startIndex = otpText.startIndex + let charIndex = otpText.index(startIndex, offsetBy: index) + let charToString = String(otpText[charIndex]) + Text(charToString) + } else { + Text(" ") + } + } + .frame(width: 45, height: 45) + .background { + let status = (isKeyboardShowing && otpText.count == index) + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(status ? activeIndicatorColor : inactiveIndicatorColor) + .animation(.easeInOut(duration: 0.2), value: status) + + } + .padding() + } +} + +@available(iOS 13.0, *) +extension Binding where Value == String { + func limit(_ length: Int)->Self { + if self.wrappedValue.count > length { + DispatchQueue.main.async { + self.wrappedValue = String(self.wrappedValue.prefix(length)) + } + } + return self + } +} diff --git a/Sources/Web3Modal/Sheets/Web3ModalView.swift b/Sources/Web3Modal/Sheets/Web3ModalView.swift index c80316a..0fe2f72 100644 --- a/Sources/Web3Modal/Sheets/Web3ModalView.swift +++ b/Sources/Web3Modal/Sheets/Web3ModalView.swift @@ -23,6 +23,8 @@ struct Web3ModalView: View { EmptyView() case .connectWallet: ConnectWalletView() + case .otp: + EnterOTPView() case .allWallets: if #available(iOS 14.0, *) { AllWalletsView() @@ -114,6 +116,8 @@ extension Router.ConnectingSubpage { return "Connect wallet" case .qr: return "WalletConnect" + case .otp: + return "Confirm Email" case .allWallets: return "All wallets" case .whatIsAWallet: From fc4fc76b2c39768da7cbde1ef8d5f77bb90ed1ae Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Fri, 2 Feb 2024 14:54:56 +0100 Subject: [PATCH 4/6] Clean up supporting message structs --- Sources/Web3Modal/Core/MagicMessage.swift | 275 --------------------- Sources/Web3Modal/Core/MagicRequest.swift | 27 ++ Sources/Web3Modal/Core/MagicResponse.swift | 193 +++++++++++++++ Sources/Web3Modal/Core/MagicService.swift | 69 +++++- 4 files changed, 276 insertions(+), 288 deletions(-) delete mode 100644 Sources/Web3Modal/Core/MagicMessage.swift create mode 100644 Sources/Web3Modal/Core/MagicRequest.swift create mode 100644 Sources/Web3Modal/Core/MagicResponse.swift diff --git a/Sources/Web3Modal/Core/MagicMessage.swift b/Sources/Web3Modal/Core/MagicMessage.swift deleted file mode 100644 index 5a7030c..0000000 --- a/Sources/Web3Modal/Core/MagicMessage.swift +++ /dev/null @@ -1,275 +0,0 @@ -import Foundation - - -protocol MagicMessageType: Codable { - var rawValue: String { get } -} - -enum MagicMessageResponseType: String, MagicMessageType, Codable { - case syncThemeSuccess = "@w3m-frame/SYNC_THEME_SUCCESS" - case syncDataSuccess = "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" - case connectEmailSuccess = "@w3m-frame/CONNECT_EMAIL_SUCCESS" - case connectEmailError = "@w3m-frame/CONNECT_EMAIL_ERROR" - case isConnectSuccess = "@w3m-frame/IS_CONNECTED_SUCCESS" - case isConnectError = "@w3m-frame/IS_CONNECTED_ERROR" - case connectOtpSuccess = "@w3m-frame/CONNECT_OTP_SUCCESS" - case connectOtpError = "@w3m-frame/CONNECT_OTP_ERROR" - case getUserSuccess = "@w3m-frame/GET_USER_SUCCESS" - case getUserError = "@w3m-frame/GET_USER_ERROR" - case sessionUpdate = "@w3m-frame/SESSION_UPDATE" - case switchNetworkSuccess = "@w3m-frame/SWITCH_NETWORK_SUCCESS" - case switchNetworkError = "@w3m-frame/SWITCH_NETWORK_ERROR" - case rpcRequestSuccess = "@w3m-frame/RPC_REQUEST_SUCCESS" - case rpcRequestError = "@w3m-frame/RPC_REQUEST_ERROR" -} - -enum MagicMessageRequestType: String, MagicMessageType, Codable { - case syncTheme = "@w3m-app/SYNC_THEME" - case syncData = "@w3m-app/SYNC_DAPP_DATA" - case connectEmail = "@w3m-app/CONNECT_EMAIL" - case connectDevice = "@w3m-app/CONNECT_DEVICE" - case isConnected = "@w3m-app/IS_CONNECTED" - case connectOtp = "@w3m-app/CONNECT_OTP" - case switchNetwork = "@w3m-app/SWITCH_NETWORK" - case rpcRequest = "@w3m-app/RPC_REQUEST" - case signOut = "@w3m-app/SIGN_OUT" - case getUser = "@w3m-app/GET_USER" - case getChainId = "@w3m-app/GET_CHAIN_ID" - case updateEmail = "@w3m-app/UPDATE_EMAIL" -} - -class MagicMessage: Decodable { - var type: MagicMessageType - var payload: AnyCodable? - var rt: String? - var jwt: String? - var action: String? - - enum CodingKeys: CodingKey { - case type - case payload - case rt - case jwt - case action - } - - init(type: MagicMessageType, payload: AnyCodable? = nil, rt: String? = nil, jwt: String? = nil, action: String? = nil) { - self.type = type - self.payload = payload - self.rt = rt - self.jwt = jwt - self.action = action - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.type = try container.decode(MagicMessageResponseType.self, forKey: .type) - self.payload = try container.decodeIfPresent(AnyCodable.self, forKey: .payload) - self.rt = try container.decodeIfPresent(String.self, forKey: .rt) - self.jwt = try container.decodeIfPresent(String.self, forKey: .jwt) - self.action = try container.decodeIfPresent(String.self, forKey: .action) - } - - var toString: String { return "{type: \"\(type)\"}" } -} - -class IsConnected: MagicMessage { - init() { - super.init(type: MagicMessageRequestType.isConnected) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -class SwitchNetwork: MagicMessage { - let chainId: String - - init(chainId: String) { - self.chainId = chainId - super.init(type: MagicMessageRequestType.switchNetwork) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - let container = try decoder.container(keyedBy: CodingKeys.self) - } - - override var toString: String { - "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" - } -} - -class ConnectEmail: MagicMessage { - let email: String - - init(email: String) { - self.email = email - super.init(type: MagicMessageRequestType.connectEmail) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - "{type:'\(type)',payload:{email:'\(email)'}}" - } -} - -class ConnectDevice: MagicMessage { - init() { - super.init(type: MagicMessageRequestType.connectDevice) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -class ConnectOtp: MagicMessage { - let otp: String - - init(otp: String) { - self.otp = otp - super.init(type: MagicMessageRequestType.connectOtp) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - "{type:'\(type)',payload:{otp:'\(otp)'}}" - } -} - -class GetUser: MagicMessage { - - let chainId: String? - - init(chainId: String?) { - self.chainId = chainId - super.init(type: MagicMessageRequestType.getUser) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - if let chainId { - return "{type:'\(type)',payload:{chainId:\(chainId)}}" - } else { - return "{type:'\(type)'}" - } - } -} - -class SignOut: MagicMessage { - init() { - super.init(type: MagicMessageRequestType.signOut) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -class GetChainId: MagicMessage { - init() { - super.init(type: MagicMessageRequestType.getChainId) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -class RpcRequest: MagicMessage { - let method: String - let params: [Any] - - init(method: String, params: [Any]) { - self.method = method - self.params = params - super.init(type: MagicMessageRequestType.rpcRequest) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - let m = "method:'\(method)'" - let p = params.map { "'\($0)'" }.joined(separator: ",") - return "{type:'\(type)',payload:{\(m),params:[\(p)]}}" - } -} - -// readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; -class UpdateEmail: MagicMessage { - init() { - super.init(type: MagicMessageRequestType.updateEmail) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } -} - -class SyncTheme: MagicMessage { - var mode: String - - init(mode: String = "light") { - self.mode = mode - super.init(type: MagicMessageRequestType.syncTheme) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - let tm = "themeMode:'\(mode)'" - return "{type:'\(type)',payload:{\(tm)}}" - } -} - -struct PairingMetadata { - let name: String - let description: String - let url: String - let icons: [String] -} - -class SyncAppData: MagicMessage { - let metadata: PairingMetadata - let sdkVersion: String - let projectId: String - - init(metadata: PairingMetadata, sdkVersion: String, projectId: String) { - self.metadata = metadata - self.sdkVersion = sdkVersion - self.projectId = projectId - super.init(type: MagicMessageRequestType.syncData) - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - override var toString: String { - let v = "verified: true" - let p1 = "projectId:'\(projectId)'" - let p2 = "sdkVersion:'\(sdkVersion)'" - let m1 = "name:'\(metadata.name)'" - let m2 = "description:'\(metadata.description)'" - let m3 = "url:'\(metadata.url)'" - let m4 = "icons:[\"\(metadata.icons.first ?? "")\"]" - let p3 = "metadata:{\(m1),\(m2),\(m3),\(m4)}" - let p = "payload:{\(v),\(p1),\(p2),\(p3)}" - return "{type:'\(type)',\(p)}" - } -} diff --git a/Sources/Web3Modal/Core/MagicRequest.swift b/Sources/Web3Modal/Core/MagicRequest.swift new file mode 100644 index 0000000..73fc066 --- /dev/null +++ b/Sources/Web3Modal/Core/MagicRequest.swift @@ -0,0 +1,27 @@ +import Foundation + +struct MagicResponse: Decodable { + enum MessageType: String, Decodable { + case syncThemeSuccess = "@w3m-frame/SYNC_THEME_SUCCESS" + case syncDataSuccess = "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" + case connectEmailSuccess = "@w3m-frame/CONNECT_EMAIL_SUCCESS" + case connectEmailError = "@w3m-frame/CONNECT_EMAIL_ERROR" + case isConnectSuccess = "@w3m-frame/IS_CONNECTED_SUCCESS" + case isConnectError = "@w3m-frame/IS_CONNECTED_ERROR" + case connectOtpSuccess = "@w3m-frame/CONNECT_OTP_SUCCESS" + case connectOtpError = "@w3m-frame/CONNECT_OTP_ERROR" + case getUserSuccess = "@w3m-frame/GET_USER_SUCCESS" + case getUserError = "@w3m-frame/GET_USER_ERROR" + case sessionUpdate = "@w3m-frame/SESSION_UPDATE" + case switchNetworkSuccess = "@w3m-frame/SWITCH_NETWORK_SUCCESS" + case switchNetworkError = "@w3m-frame/SWITCH_NETWORK_ERROR" + case rpcRequestSuccess = "@w3m-frame/RPC_REQUEST_SUCCESS" + case rpcRequestError = "@w3m-frame/RPC_REQUEST_ERROR" + } + + var type: MessageType + var payload: AnyCodable? + var rt: String? + var jwt: String? + var action: String? +} diff --git a/Sources/Web3Modal/Core/MagicResponse.swift b/Sources/Web3Modal/Core/MagicResponse.swift new file mode 100644 index 0000000..9abf3aa --- /dev/null +++ b/Sources/Web3Modal/Core/MagicResponse.swift @@ -0,0 +1,193 @@ +import Foundation +import WalletConnectSign + +protocol MagicMessageRequest { + var type: MagicRequest.MessageType { get } + var toString: String { get } +} + +extension MagicMessageRequest { + var toString: String { + return #" { "type": "\#(type)"} "# + } +} + +enum MagicRequest { + + enum MessageType: String { + case syncTheme = "@w3m-app/SYNC_THEME" + case syncData = "@w3m-app/SYNC_DAPP_DATA" + case connectEmail = "@w3m-app/CONNECT_EMAIL" + case connectDevice = "@w3m-app/CONNECT_DEVICE" + case isConnected = "@w3m-app/IS_CONNECTED" + case connectOtp = "@w3m-app/CONNECT_OTP" + case switchNetwork = "@w3m-app/SWITCH_NETWORK" + case rpcRequest = "@w3m-app/RPC_REQUEST" + case signOut = "@w3m-app/SIGN_OUT" + case getUser = "@w3m-app/GET_USER" + case getChainId = "@w3m-app/GET_CHAIN_ID" + case updateEmail = "@w3m-app/UPDATE_EMAIL" + } + + struct IsConnected: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.isConnected + } + } + + struct SwitchNetwork: MagicMessageRequest { + let chainId: String + let type: MessageType + + init(chainId: String) { + self.chainId = chainId + self.type = MessageType.switchNetwork + } + + var toString: String { + "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" + } + } + + struct ConnectEmail: MagicMessageRequest { + let type: MessageType + let email: String + + init(email: String) { + self.email = email + self.type = MessageType.connectEmail + } + + var toString: String { + "{type:'\(type)',payload:{email:'\(email)'}}" + } + } + + struct ConnectDevice: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.connectDevice + } + } + + struct ConnectOtp: MagicMessageRequest { + let otp: String + let type: MessageType + + init(otp: String) { + self.otp = otp + self.type = MessageType.connectOtp + } + + var toString: String { + "{type:'\(type)',payload:{otp:'\(otp)'}}" + } + } + + struct GetUser: MagicMessageRequest { + let type: MessageType + let chainId: String? + + init(chainId: String?) { + self.chainId = chainId + self.type = MessageType.getUser + } + + var toString: String { + if let chainId { + return "{type:'\(type)',payload:{chainId:\(chainId)}}" + } else { + return "{type:'\(type)'}" + } + } + } + + struct SignOut: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.signOut + } + } + + struct GetChainId: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.getChainId + } + } + + struct RpcRequest: MagicMessageRequest { + let method: String + let params: AnyCodable + let type: MessageType + + init(method: String, params: AnyCodable) { + self.method = method + self.params = params + self.type = MessageType.rpcRequest + } + + // TODO: Properly convert params to string + var toString: String { + let m = "method:'\(method)'" + let p = "" // params.map { "'\($0)'" }.joined(separator: ",") + return "{type:'\(type)',payload:{\(m),params:[\(p)]}}" + } + } + + // readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; + struct UpdateEmail: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.updateEmail + } + } + + struct SyncTheme: MagicMessageRequest { + var mode: String + let type: MessageType + + init(mode: String = "light") { + self.mode = mode + self.type = MessageType.syncTheme + } + + var toString: String { + let tm = "themeMode:'\(mode)'" + return "{type:'\(type)',payload:{\(tm)}}" + } + } + + struct SyncAppData: MagicMessageRequest { + let metadata: AppMetadata + let sdkVersion: String + let projectId: String + let type: MessageType + + init(metadata: AppMetadata, sdkVersion: String, projectId: String) { + self.metadata = metadata + self.sdkVersion = sdkVersion + self.projectId = projectId + self.type = MessageType.syncData + } + + var toString: String { + let v = "verified: true" + let p1 = "projectId:'\(projectId)'" + let p2 = "sdkVersion:'\(sdkVersion)'" + let m1 = "name:'\(metadata.name)'" + let m2 = "description:'\(metadata.description)'" + let m3 = "url:'\(metadata.url)'" + let m4 = "icons:[\"\(metadata.icons.first ?? "")\"]" + let p3 = "metadata:{\(m1),\(m2),\(m3),\(m4)}" + let p = "payload:{\(v),\(p1),\(p2),\(p3)}" + return "{type:'\(type)',\(p)}" + } + } +} diff --git a/Sources/Web3Modal/Core/MagicService.swift b/Sources/Web3Modal/Core/MagicService.swift index 2c63266..7673915 100644 --- a/Sources/Web3Modal/Core/MagicService.swift +++ b/Sources/Web3Modal/Core/MagicService.swift @@ -19,7 +19,7 @@ class MagicService { private let url = "https://secure.walletconnect.com" private let projectId: String = Web3Modal.config.projectId -// private let metadata: PairingMetadata + private let metadata: AppMetadata = Web3Modal.config.metadata private let messageHandler: MessageHandler private let navigationDelegate: NavigationDelegate @@ -80,29 +80,29 @@ class MagicService { } func connectEmail(email: String) async { - let message = ConnectEmail(email: email).toString + let message = MagicRequest.ConnectEmail(email: email).toString await runJavaScript("sendMessage(\(message))") } func connectDevice() async { - let message = ConnectDevice().toString + let message = MagicRequest.ConnectDevice().toString await runJavaScript("sendMessage(\(message))") } func connectOtp(otp: String) async { // Assuming waitConfirmation is a property you're using to track state // waitConfirmation.value = true - let message = ConnectOtp(otp: otp).toString + let message = MagicRequest.ConnectOtp(otp: otp).toString await runJavaScript("sendMessage(\(message))") } func isConnected() async { - let message = IsConnected().toString + let message = MagicRequest.IsConnected().toString await runJavaScript("sendMessage(\(message))") } func getChainId() async { - let message = GetChainId().toString + let message = MagicRequest.GetChainId().toString await runJavaScript("sendMessage(\(message))") } @@ -113,22 +113,26 @@ class MagicService { func syncTheme(theme: Web3ModalTheme?) async { guard let mode = theme?.rawValue else { return } - let message = SyncTheme(mode: mode) + let message = MagicRequest.SyncTheme(mode: mode) await runJavaScript("sendMessage(\(message))") } - func syncDappData(metadata: PairingMetadata, sdkVersion: String, projectId: String) async { - let message = SyncAppData(metadata: metadata, sdkVersion: sdkVersion, projectId: projectId).toString + func syncDappData( + metadata: AppMetadata, + sdkVersion: String, + projectId: String + ) async { + let message = MagicRequest.SyncAppData(metadata: metadata, sdkVersion: sdkVersion, projectId: projectId).toString await runJavaScript("sendMessage(\(message))") } func getUser(chainId: String?) async { - let message = GetUser(chainId: chainId).toString + let message = MagicRequest.GetUser(chainId: chainId).toString await runJavaScript("sendMessage(\(message))") } func switchNetwork(chainId: String) async { - let message = SwitchNetwork(chainId: chainId).toString + let message = MagicRequest.SwitchNetwork(chainId: chainId).toString await runJavaScript("sendMessage(\(message))") } @@ -169,12 +173,51 @@ class MessageHandler: NSObject, WKScriptMessageHandler { let data = bodyString.data(using: .utf8) else { return } do { - let jsonMessage = try JSONDecoder().decode(MagicMessage.self, from: data) - // Handle the decoded message + let response = try JSONDecoder().decode(MagicResponse.self, from: data) + handleMagicResponse(response) } catch { print("Error decoding message: \(error)") } } + + func handleMagicResponse(_ response: MagicResponse) { + + print(response) + + switch response.type { + case .syncThemeSuccess: + break + case .syncDataSuccess: + break + case .connectEmailSuccess: + break + case .connectEmailError: + break + case .isConnectSuccess: + break + case .isConnectError: + break + case .connectOtpSuccess: + break + case .connectOtpError: + break + case .getUserSuccess: + break + case .getUserError: + break + case .sessionUpdate: + break + case .switchNetworkSuccess: + break + case .switchNetworkError: + break + case .rpcRequestSuccess: + break + case .rpcRequestError: + break + } + } + } class NavigationDelegate: NSObject, WKNavigationDelegate { From c379969ba5833ce199764a240c2dfbeb661168e9 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Fri, 2 Feb 2024 17:48:28 +0100 Subject: [PATCH 5/6] Implement OTP input screen and improve previews --- Sources/Web3Modal/Core/MagicMessage.swift | 27 +++ .../Web3Modal/Core/MagicMessageHandler.swift | 85 +++++++ Sources/Web3Modal/Core/MagicRequest.swift | 210 ++++++++++++++++-- Sources/Web3Modal/Core/MagicResponse.swift | 193 ---------------- Sources/Web3Modal/Core/MagicService.swift | 62 +----- Sources/Web3Modal/Core/Web3Modal.swift | 34 ++- .../Web3Modal/Helpers/PreviewSupport.swift | 91 ++++++++ Sources/Web3Modal/Router.swift | 7 +- .../Screens/ConnectWallet/EnterOTPView.swift | 165 ++++++++++++++ .../Screens/ConnectWallet/SwiftUIView.swift | 115 ---------- .../Web3Modal/Sheets/ModalContainerView.swift | 8 +- Sources/Web3Modal/Sheets/Web3ModalView.swift | 94 +++++++- Sources/Web3Modal/Store.swift | 16 +- 13 files changed, 690 insertions(+), 417 deletions(-) create mode 100644 Sources/Web3Modal/Core/MagicMessage.swift create mode 100644 Sources/Web3Modal/Core/MagicMessageHandler.swift delete mode 100644 Sources/Web3Modal/Core/MagicResponse.swift create mode 100644 Sources/Web3Modal/Helpers/PreviewSupport.swift create mode 100644 Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift delete mode 100644 Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift diff --git a/Sources/Web3Modal/Core/MagicMessage.swift b/Sources/Web3Modal/Core/MagicMessage.swift new file mode 100644 index 0000000..338e9c8 --- /dev/null +++ b/Sources/Web3Modal/Core/MagicMessage.swift @@ -0,0 +1,27 @@ +import Foundation + +struct MagicMessage: Decodable { + enum MessageType: String, Decodable { + case syncThemeSuccess = "@w3m-frame/SYNC_THEME_SUCCESS" + case syncDataSuccess = "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" + case connectEmailSuccess = "@w3m-frame/CONNECT_EMAIL_SUCCESS" + case connectEmailError = "@w3m-frame/CONNECT_EMAIL_ERROR" + case isConnectSuccess = "@w3m-frame/IS_CONNECTED_SUCCESS" + case isConnectError = "@w3m-frame/IS_CONNECTED_ERROR" + case connectOtpSuccess = "@w3m-frame/CONNECT_OTP_SUCCESS" + case connectOtpError = "@w3m-frame/CONNECT_OTP_ERROR" + case getUserSuccess = "@w3m-frame/GET_USER_SUCCESS" + case getUserError = "@w3m-frame/GET_USER_ERROR" + case sessionUpdate = "@w3m-frame/SESSION_UPDATE" + case switchNetworkSuccess = "@w3m-frame/SWITCH_NETWORK_SUCCESS" + case switchNetworkError = "@w3m-frame/SWITCH_NETWORK_ERROR" + case rpcRequestSuccess = "@w3m-frame/RPC_REQUEST_SUCCESS" + case rpcRequestError = "@w3m-frame/RPC_REQUEST_ERROR" + } + + var type: String + var payload: AnyCodable? + var rt: String? + var jwt: String? + var action: String? +} diff --git a/Sources/Web3Modal/Core/MagicMessageHandler.swift b/Sources/Web3Modal/Core/MagicMessageHandler.swift new file mode 100644 index 0000000..ed07efd --- /dev/null +++ b/Sources/Web3Modal/Core/MagicMessageHandler.swift @@ -0,0 +1,85 @@ +import Foundation +import WebKit + +class MagicMessageHandler: NSObject, WKScriptMessageHandler { + + private let router: Router + private let store: Store + + init(router: Router, store: Store = .shared) { + self.router = router + self.store = store + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + + print(message.body) + + guard + let bodyString = message.body as? String, + bodyString.contains("@w3m-frame"), + let data = bodyString.data(using: .utf8) + else { + return + } + + do { + let response = try JSONDecoder().decode(MagicMessage.self, from: data) + handleMagicResponse(response) + } catch { + print("Failed decoding Magic response: ", error) + } + } + + struct Payload: Codable { + let action: String? + } + + func handleMagicResponse(_ response: MagicMessage) { + + let payload = try? response.payload?.get(Payload.self) + + switch MagicMessage.MessageType(rawValue: response.type) { + case .syncThemeSuccess: + break + case .syncDataSuccess: + break + case .connectEmailSuccess: + switch payload?.action { + case "VERIFY_OTP": + router.setRoute(Router.ConnectingSubpage.otpInput) + case "VERIFY_DEVICE": + router.setRoute(Router.ConnectingSubpage.verifyDevice) + default: + break + } + case .connectEmailError: + break + case .isConnectSuccess: + break + case .isConnectError: + break + case .connectOtpSuccess: + router.setRoute(Router.ConnectingSubpage.otpResult(true)) + case .connectOtpError: + router.setRoute(Router.ConnectingSubpage.otpResult(false)) + case .getUserSuccess: + print("getUserSuccess") + case .getUserError: + print("getUserError") + case .sessionUpdate: + break + case .switchNetworkSuccess: + break + case .switchNetworkError: + break + case .rpcRequestSuccess: + break + case .rpcRequestError: + break + case .none: + break + } + } + +} diff --git a/Sources/Web3Modal/Core/MagicRequest.swift b/Sources/Web3Modal/Core/MagicRequest.swift index 73fc066..6f201b2 100644 --- a/Sources/Web3Modal/Core/MagicRequest.swift +++ b/Sources/Web3Modal/Core/MagicRequest.swift @@ -1,27 +1,193 @@ import Foundation +import WalletConnectSign -struct MagicResponse: Decodable { - enum MessageType: String, Decodable { - case syncThemeSuccess = "@w3m-frame/SYNC_THEME_SUCCESS" - case syncDataSuccess = "@w3m-frame/SYNC_DAPP_DATA_SUCCESS" - case connectEmailSuccess = "@w3m-frame/CONNECT_EMAIL_SUCCESS" - case connectEmailError = "@w3m-frame/CONNECT_EMAIL_ERROR" - case isConnectSuccess = "@w3m-frame/IS_CONNECTED_SUCCESS" - case isConnectError = "@w3m-frame/IS_CONNECTED_ERROR" - case connectOtpSuccess = "@w3m-frame/CONNECT_OTP_SUCCESS" - case connectOtpError = "@w3m-frame/CONNECT_OTP_ERROR" - case getUserSuccess = "@w3m-frame/GET_USER_SUCCESS" - case getUserError = "@w3m-frame/GET_USER_ERROR" - case sessionUpdate = "@w3m-frame/SESSION_UPDATE" - case switchNetworkSuccess = "@w3m-frame/SWITCH_NETWORK_SUCCESS" - case switchNetworkError = "@w3m-frame/SWITCH_NETWORK_ERROR" - case rpcRequestSuccess = "@w3m-frame/RPC_REQUEST_SUCCESS" - case rpcRequestError = "@w3m-frame/RPC_REQUEST_ERROR" +protocol MagicMessageRequest { + var type: MagicRequest.MessageType { get } + var toString: String { get } +} + +extension MagicMessageRequest { + var toString: String { + return #" { "type": "\#(type.rawValue)"} "# + } +} + +enum MagicRequest { + + enum MessageType: String { + case syncTheme = "@w3m-app/SYNC_THEME" + case syncData = "@w3m-app/SYNC_DAPP_DATA" + case connectEmail = "@w3m-app/CONNECT_EMAIL" + case connectDevice = "@w3m-app/CONNECT_DEVICE" + case isConnected = "@w3m-app/IS_CONNECTED" + case connectOtp = "@w3m-app/CONNECT_OTP" + case switchNetwork = "@w3m-app/SWITCH_NETWORK" + case rpcRequest = "@w3m-app/RPC_REQUEST" + case signOut = "@w3m-app/SIGN_OUT" + case getUser = "@w3m-app/GET_USER" + case getChainId = "@w3m-app/GET_CHAIN_ID" + case updateEmail = "@w3m-app/UPDATE_EMAIL" + } + + struct IsConnected: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.isConnected + } + } + + struct SwitchNetwork: MagicMessageRequest { + let chainId: String + let type: MessageType + + init(chainId: String) { + self.chainId = chainId + self.type = MessageType.switchNetwork + } + + var toString: String { + "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" + } + } + + struct ConnectEmail: MagicMessageRequest { + let type: MessageType + let email: String + + init(email: String) { + self.email = email + self.type = MessageType.connectEmail + } + + var toString: String { + "{type:'\(type.rawValue)',payload:{email:'\(email)'}}" + } + } + + struct ConnectDevice: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.connectDevice + } + } + + struct ConnectOtp: MagicMessageRequest { + let otp: String + let type: MessageType + + init(otp: String) { + self.otp = otp + self.type = MessageType.connectOtp + } + + var toString: String { + "{type:'\(type.rawValue)',payload:{otp:'\(otp)'}}" + } + } + + struct GetUser: MagicMessageRequest { + let type: MessageType + let chainId: String? + + init(chainId: String?) { + self.chainId = chainId + self.type = MessageType.getUser + } + + var toString: String { + if let chainId { + return "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" + } else { + return "{type:'\(type.rawValue)'}" + } + } + } + + struct SignOut: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.signOut + } + } + + struct GetChainId: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.getChainId + } + } + + struct RpcRequest: MagicMessageRequest { + let method: String + let params: AnyCodable + let type: MessageType + + init(method: String, params: AnyCodable) { + self.method = method + self.params = params + self.type = MessageType.rpcRequest + } + + // TODO: Properly convert params to string + var toString: String { + let m = "method:'\(method)'" + let p = "" // params.map { "'\($0)'" }.joined(separator: ",") + return "{type:'\(type.rawValue)',payload:{\(m),params:[\(p)]}}" + } + } + + // readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; + struct UpdateEmail: MagicMessageRequest { + let type: MessageType + + init() { + self.type = MessageType.updateEmail + } + } + + struct SyncTheme: MagicMessageRequest { + var mode: String + let type: MessageType + + init(mode: String = "light") { + self.mode = mode + self.type = MessageType.syncTheme + } + + var toString: String { + let tm = "themeMode:'\(mode)'" + return "{type:'\(type.rawValue)',payload:{\(tm)}}" + } } - var type: MessageType - var payload: AnyCodable? - var rt: String? - var jwt: String? - var action: String? + struct SyncAppData: MagicMessageRequest { + let metadata: AppMetadata + let sdkVersion: String + let projectId: String + let type: MessageType + + init(metadata: AppMetadata, sdkVersion: String, projectId: String) { + self.metadata = metadata + self.sdkVersion = sdkVersion + self.projectId = projectId + self.type = MessageType.syncData + } + + var toString: String { + let v = "verified: true" + let p1 = "projectId:'\(projectId)'" + let p2 = "sdkVersion:'\(sdkVersion)'" + let m1 = "name:'\(metadata.name)'" + let m2 = "description:'\(metadata.description)'" + let m3 = "url:'\(metadata.url)'" + let m4 = "icons:[\"\(metadata.icons.first ?? "")\"]" + let p3 = "metadata:{\(m1),\(m2),\(m3),\(m4)}" + let p = "payload:{\(v),\(p1),\(p2),\(p3)}" + return "{type:'\(type.rawValue)',\(p)}" + } + } } diff --git a/Sources/Web3Modal/Core/MagicResponse.swift b/Sources/Web3Modal/Core/MagicResponse.swift deleted file mode 100644 index 9abf3aa..0000000 --- a/Sources/Web3Modal/Core/MagicResponse.swift +++ /dev/null @@ -1,193 +0,0 @@ -import Foundation -import WalletConnectSign - -protocol MagicMessageRequest { - var type: MagicRequest.MessageType { get } - var toString: String { get } -} - -extension MagicMessageRequest { - var toString: String { - return #" { "type": "\#(type)"} "# - } -} - -enum MagicRequest { - - enum MessageType: String { - case syncTheme = "@w3m-app/SYNC_THEME" - case syncData = "@w3m-app/SYNC_DAPP_DATA" - case connectEmail = "@w3m-app/CONNECT_EMAIL" - case connectDevice = "@w3m-app/CONNECT_DEVICE" - case isConnected = "@w3m-app/IS_CONNECTED" - case connectOtp = "@w3m-app/CONNECT_OTP" - case switchNetwork = "@w3m-app/SWITCH_NETWORK" - case rpcRequest = "@w3m-app/RPC_REQUEST" - case signOut = "@w3m-app/SIGN_OUT" - case getUser = "@w3m-app/GET_USER" - case getChainId = "@w3m-app/GET_CHAIN_ID" - case updateEmail = "@w3m-app/UPDATE_EMAIL" - } - - struct IsConnected: MagicMessageRequest { - let type: MessageType - - init() { - self.type = MessageType.isConnected - } - } - - struct SwitchNetwork: MagicMessageRequest { - let chainId: String - let type: MessageType - - init(chainId: String) { - self.chainId = chainId - self.type = MessageType.switchNetwork - } - - var toString: String { - "{type:'\(type.rawValue)',payload:{chainId:\(chainId)}}" - } - } - - struct ConnectEmail: MagicMessageRequest { - let type: MessageType - let email: String - - init(email: String) { - self.email = email - self.type = MessageType.connectEmail - } - - var toString: String { - "{type:'\(type)',payload:{email:'\(email)'}}" - } - } - - struct ConnectDevice: MagicMessageRequest { - let type: MessageType - - init() { - self.type = MessageType.connectDevice - } - } - - struct ConnectOtp: MagicMessageRequest { - let otp: String - let type: MessageType - - init(otp: String) { - self.otp = otp - self.type = MessageType.connectOtp - } - - var toString: String { - "{type:'\(type)',payload:{otp:'\(otp)'}}" - } - } - - struct GetUser: MagicMessageRequest { - let type: MessageType - let chainId: String? - - init(chainId: String?) { - self.chainId = chainId - self.type = MessageType.getUser - } - - var toString: String { - if let chainId { - return "{type:'\(type)',payload:{chainId:\(chainId)}}" - } else { - return "{type:'\(type)'}" - } - } - } - - struct SignOut: MagicMessageRequest { - let type: MessageType - - init() { - self.type = MessageType.signOut - } - } - - struct GetChainId: MagicMessageRequest { - let type: MessageType - - init() { - self.type = MessageType.getChainId - } - } - - struct RpcRequest: MagicMessageRequest { - let method: String - let params: AnyCodable - let type: MessageType - - init(method: String, params: AnyCodable) { - self.method = method - self.params = params - self.type = MessageType.rpcRequest - } - - // TODO: Properly convert params to string - var toString: String { - let m = "method:'\(method)'" - let p = "" // params.map { "'\($0)'" }.joined(separator: ",") - return "{type:'\(type)',payload:{\(m),params:[\(p)]}}" - } - } - - // readonly APP_AWAIT_UPDATE_EMAIL: "@w3m-app/AWAIT_UPDATE_EMAIL"; - struct UpdateEmail: MagicMessageRequest { - let type: MessageType - - init() { - self.type = MessageType.updateEmail - } - } - - struct SyncTheme: MagicMessageRequest { - var mode: String - let type: MessageType - - init(mode: String = "light") { - self.mode = mode - self.type = MessageType.syncTheme - } - - var toString: String { - let tm = "themeMode:'\(mode)'" - return "{type:'\(type)',payload:{\(tm)}}" - } - } - - struct SyncAppData: MagicMessageRequest { - let metadata: AppMetadata - let sdkVersion: String - let projectId: String - let type: MessageType - - init(metadata: AppMetadata, sdkVersion: String, projectId: String) { - self.metadata = metadata - self.sdkVersion = sdkVersion - self.projectId = projectId - self.type = MessageType.syncData - } - - var toString: String { - let v = "verified: true" - let p1 = "projectId:'\(projectId)'" - let p2 = "sdkVersion:'\(sdkVersion)'" - let m1 = "name:'\(metadata.name)'" - let m2 = "description:'\(metadata.description)'" - let m3 = "url:'\(metadata.url)'" - let m4 = "icons:[\"\(metadata.icons.first ?? "")\"]" - let p3 = "metadata:{\(m1),\(m2),\(m3),\(m4)}" - let p = "payload:{\(v),\(p1),\(p2),\(p3)}" - return "{type:'\(type)',\(p)}" - } - } -} diff --git a/Sources/Web3Modal/Core/MagicService.swift b/Sources/Web3Modal/Core/MagicService.swift index 7673915..8824ceb 100644 --- a/Sources/Web3Modal/Core/MagicService.swift +++ b/Sources/Web3Modal/Core/MagicService.swift @@ -7,6 +7,7 @@ enum Web3ModalTheme: String { } class MagicService { + private let injectScript = """ window.addEventListener('message', ({ data }) => { window.webkit.messageHandlers.nativeProcess.postMessage(JSON.stringify(data)) @@ -21,15 +22,15 @@ class MagicService { private let projectId: String = Web3Modal.config.projectId private let metadata: AppMetadata = Web3Modal.config.metadata - private let messageHandler: MessageHandler + private let messageHandler: MagicMessageHandler private let navigationDelegate: NavigationDelegate private let webview: WKWebView private let contentController: WKUserContentController private var attachedToViewHierarchy = false - init() { - self.messageHandler = MessageHandler() + init(router: Router, store: Store = .shared) { + self.messageHandler = MagicMessageHandler(router: router, store: store) self.navigationDelegate = NavigationDelegate() self.contentController = WKUserContentController() @@ -79,7 +80,7 @@ class MagicService { attachedToViewHierarchy = true } - func connectEmail(email: String) async { + public func connectEmail(email: String) async { let message = MagicRequest.ConnectEmail(email: email).toString await runJavaScript("sendMessage(\(message))") } @@ -167,59 +168,6 @@ class MagicService { } } -class MessageHandler: NSObject, WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - guard let bodyString = message.body as? String, - let data = bodyString.data(using: .utf8) else { return } - - do { - let response = try JSONDecoder().decode(MagicResponse.self, from: data) - handleMagicResponse(response) - } catch { - print("Error decoding message: \(error)") - } - } - - func handleMagicResponse(_ response: MagicResponse) { - - print(response) - - switch response.type { - case .syncThemeSuccess: - break - case .syncDataSuccess: - break - case .connectEmailSuccess: - break - case .connectEmailError: - break - case .isConnectSuccess: - break - case .isConnectError: - break - case .connectOtpSuccess: - break - case .connectOtpError: - break - case .getUserSuccess: - break - case .getUserError: - break - case .sessionUpdate: - break - case .switchNetworkSuccess: - break - case .switchNetworkError: - break - case .rpcRequestSuccess: - break - case .rpcRequestError: - break - } - } - -} - class NavigationDelegate: NSObject, WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {} } diff --git a/Sources/Web3Modal/Core/Web3Modal.swift b/Sources/Web3Modal/Core/Web3Modal.swift index 2151770..673e420 100644 --- a/Sources/Web3Modal/Core/Web3Modal.swift +++ b/Sources/Web3Modal/Core/Web3Modal.swift @@ -98,6 +98,32 @@ public class Web3Modal { customWallets: [Wallet] = [], coinbaseEnabled: Bool = true, onError: @escaping (Error) -> Void = { _ in } + ) { + configure( + projectId: projectId, + metadata: metadata, + sessionParams: sessionParams, + includeWebWallets: includeWebWallets, + recommendedWalletIds: recommendedWalletIds, + excludedWalletIds: excludedWalletIds, + customWallets: customWallets, + coinbaseEnabled: coinbaseEnabled, + onError: onError, + store: .shared + ) + } + + static func configure( + projectId: String, + metadata: AppMetadata, + sessionParams: SessionParams = .default, + includeWebWallets: Bool = true, + recommendedWalletIds: [String] = [], + excludedWalletIds: [String] = [], + customWallets: [Wallet] = [], + coinbaseEnabled: Bool = true, + onError: @escaping (Error) -> Void = { _ in }, + store: Store = .shared ) { Pair.configure(metadata: metadata) @@ -113,7 +139,6 @@ public class Web3Modal { onError: onError ) - let store = Store.shared let router = Router() let w3mApiInteractor = W3MAPIInteractor(store: store) let signInteractor = SignInteractor(store: store) @@ -127,7 +152,7 @@ public class Web3Modal { w3mApiInteractor: w3mApiInteractor ) - Web3Modal.magicService = MagicService() + Web3Modal.magicService = MagicService(router: router, store: store) Web3Modal.viewModel = Web3ModalViewModel( router: router, @@ -154,7 +179,10 @@ public class Web3Modal { metadata: AppMetadata, w3mApiInteractor: W3MAPIInteractor ) { - guard Web3Modal.config.coinbaseEnabled else { return } + guard + Web3Modal.config.coinbaseEnabled, + CoinbaseWalletSDK.isConfigured == false + else { return } if let redirectLink = metadata.redirect?.universal ?? metadata.redirect?.native { CoinbaseWalletSDK.configure(callback: URL(string: redirectLink)!) diff --git a/Sources/Web3Modal/Helpers/PreviewSupport.swift b/Sources/Web3Modal/Helpers/PreviewSupport.swift new file mode 100644 index 0000000..2df9937 --- /dev/null +++ b/Sources/Web3Modal/Helpers/PreviewSupport.swift @@ -0,0 +1,91 @@ +import SwiftUI +import Foundation + +#if DEBUG + +class MockWebSocketConnecting: WebSocketConnecting { + init() { + self.onConnect = nil + self.onDisconnect = nil + self.onText = nil + self.request = URLRequest(url: URL(string: "www.google.com")!) + } + + var isConnected: Bool { false } + var onConnect: (() -> Void)? + var onDisconnect: ((Error?) -> Void)? + var onText: ((String) -> Void)? + var request: URLRequest + func connect() {} + func disconnect() {} + func write(string: String, completion: (() -> Void)?) {} +} + +struct MockSockFactory: WebSocketFactory { + public init() {} + + public func create(with url: URL) -> WebSocketConnecting { + return MockWebSocketConnecting() + } +} + +extension Router { + static let mock: Router = .init() + + static func mockWith(currentRoute: any SubPage) -> Router { + mock.setRoute(currentRoute) + return mock + } +} + +extension Store { + + static let mock: Store = .init() + + static func mockWith(_ mutation: (Store) -> Store) -> Store { + mutation(mock) + } +} + +extension View { + + func mockSetup() { + + let projectId = "" // your project_id goes here + + assert(projectId != "", "Please provide a project id") + + let metadata = AppMetadata( + name: "Web3Modal Swift Dapp", + description: "Web3Modal DApp sample", + url: "www.web3modal.com", + icons: ["https://avatars.githubusercontent.com/u/37784886"], + redirect: .init(native: "w3mdapp://", universal: nil) + ) + + Networking.configure( + groupIdentifier: "group.com.walletconnect.web3modal", + projectId: projectId, + socketFactory: MockSockFactory() + ) + + Web3Modal.configure( + projectId: projectId, + metadata: metadata, + store: .mock + ) + } + + func withMockSetup() -> some View { + mockSetup() + + return self + } + + func mockStore(_ mutation: (Store) -> Void) -> some View { + _ = mutation(Store.mock) + return self + } +} + +#endif diff --git a/Sources/Web3Modal/Router.swift b/Sources/Web3Modal/Router.swift index a734b42..542aa54 100644 --- a/Sources/Web3Modal/Router.swift +++ b/Sources/Web3Modal/Router.swift @@ -48,12 +48,17 @@ class Router: ObservableObject { enum ConnectingSubpage: SubPage { case connectWallet - case otp case qr case allWallets case whatIsAWallet case walletDetail(Wallet) case getWallet + + // Magic + case otpInput + case otpResult(Bool) + case verifyDevice + case magicWebview } enum NetworkSwitchSubpage: SubPage { diff --git a/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift b/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift new file mode 100644 index 0000000..0965d57 --- /dev/null +++ b/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift @@ -0,0 +1,165 @@ +import SwiftUI + +struct EnterOTPView: View { + @EnvironmentObject var store: Store + + @State var otp: String = "" + + var body: some View { + VStack(spacing: 0) { + Text("Enter the code we sent to \(store.email ?? "undefined")") + .font(.paragraph500) + .foregroundColor(.Foreground125) + + Spacer().frame(height: Spacing.s) + + Text("The code expires in 10 minutes") + .font(.small400) + .foregroundColor(.Foreground200) + + Spacer().frame(height: Spacing.l) + + otpInput() + + Spacer().frame(height: Spacing.s) + + HStack(spacing: Spacing.xxs) { + Text("Didn't receive it?") + .font(.small400) + .foregroundColor(.Foreground200) + + Text("Resend code") + .font(.small600) + .foregroundColor(.Blue100) + } + } + .padding(Spacing.l) + } + + @ViewBuilder + func otpInput() -> some View { + if #available(iOS 15.0, *) { + OtpView( + length: 6, + onUpdate: { otp in + self.otp = otp + Task { + await Web3Modal.magicService?.connectOtp(otp: otp) + } + } + ) + } else { + EmptyView() + } + } +} + +#if DEBUG +struct EnterOTPView_Previews: PreviewProvider { + + static let store = { + let store = Store() + store.email = "radek@walletconnect.com" + + return store + }() + + static var previews: some View { + EnterOTPView() + .environmentObject(EnterOTPView_Previews.store) + } +} +#endif + +@available(iOS 15.0, *) +public struct OtpView: View { + private let onUpdate: (String) -> Void + private let length: Int + + @State private var otpText = "" + @FocusState private var isKeyboardShowing: Bool + + public init( + length: Int, + onUpdate: @escaping (String) -> Void + + ) { + self.length = length + self.onUpdate = onUpdate + } + + public var body: some View { + HStack(spacing: Spacing.s) { + ForEach(0 ... length - 1, id: \.self) { index in + OTPTextBox(index) + } + } + .background(content: { + TextField("", text: $otpText) + .keyboardType(.numberPad) + .textContentType(.oneTimeCode) + .frame(width: 1, height: 1) + .opacity(0.001) + .blendMode(.screen) + .focused($isKeyboardShowing) + .onChange(of: otpText) { newValue in + if newValue.count == length { + onUpdate(newValue) + } + } + .onAppear { + DispatchQueue.main.async { + isKeyboardShowing = true + } + } + }) + .contentShape(Rectangle()) + .onTapGesture { + isKeyboardShowing = true + } + } + + @ViewBuilder + func OTPTextBox(_ index: Int) -> some View { + let isEditing = (isKeyboardShowing && otpText.count == index) + + ZStack { + if otpText.count > index { + let startIndex = otpText.startIndex + let charIndex = otpText.index(startIndex, offsetBy: index) + let charToString = String(otpText[charIndex]) + Text(charToString) + } else { + Text(isEditing ? "|" : "") + .foregroundColor(.Blue100) + } + } + .frame(width: 50, height: 50) + .foregroundColor(.Foreground100) + .font(.large500) + .background( + RoundedRectangle(cornerRadius: Radius.xs) + .fill(isEditing ? .GrayGlass010 : .GrayGlass005) + ) + .background( + RoundedRectangle(cornerRadius: Radius.xs) + .stroke(.GrayGlass005, lineWidth: 1) + ) + .backport.overlay { + if isEditing { + ZStack { + RoundedRectangle(cornerRadius: Radius.xs) + .inset(by: -4) + .fill(Color(red: 0.2, green: 0.59, blue: 1).opacity(0.2)) + + RoundedRectangle(cornerRadius: Radius.xs) + .blendMode(.destinationOut) + + RoundedRectangle(cornerRadius: Radius.xs) + .stroke(Color.Blue100, lineWidth: 1) + } + .compositingGroup() + } + } + } +} diff --git a/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift b/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift deleted file mode 100644 index a31a220..0000000 --- a/Sources/Web3Modal/Screens/ConnectWallet/SwiftUIView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import SwiftUI - -struct EnterOTPView: View { - var body: some View { - if #available(iOS 15.0, *) { - OtpView( - activeIndicatorColor: .blue, - inactiveIndicatorColor: .gray, - length: 6, - doSomething: { otp in - print(otp) - } - ) - } else { - EmptyView() - } - } -} - -#if DEBUG -struct EnterOTPView_Previews: PreviewProvider { - static var previews: some View { - EnterOTPView() - } -} -#endif - - -@available(iOS 15.0, *) -public struct OtpView:View { - - private var activeIndicatorColor: Color - private var inactiveIndicatorColor: Color - private let doSomething: (String) -> Void - private let length: Int - - @State private var otpText = "" - @FocusState private var isKeyboardShowing: Bool - - public init( - activeIndicatorColor: Color, - inactiveIndicatorColor: Color, - length: Int, - doSomething: @escaping (String) -> Void - ) { - self.activeIndicatorColor = activeIndicatorColor - self.inactiveIndicatorColor = inactiveIndicatorColor - self.length = length - self.doSomething = doSomething - } - public var body: some View { - HStack(spacing: 0){ - ForEach(0...length-1, id: \.self) { index in - OTPTextBox(index) - } - }.background(content: { - TextField("", text: $otpText.limit(length)) - .keyboardType(.numberPad) - .textContentType(.oneTimeCode) - .frame(width: 1, height: 1) - .opacity(0.001) - .blendMode(.screen) - .focused($isKeyboardShowing) - .onChange(of: otpText) { newValue in - if newValue.count == length { - doSomething(newValue) - } - } - .onAppear { - DispatchQueue.main.async { - isKeyboardShowing = true - } - } - }) - .contentShape(Rectangle()) - .onTapGesture { - isKeyboardShowing = true - } - } - - @ViewBuilder - func OTPTextBox(_ index: Int) -> some View { - ZStack{ - if otpText.count > index { - let startIndex = otpText.startIndex - let charIndex = otpText.index(startIndex, offsetBy: index) - let charToString = String(otpText[charIndex]) - Text(charToString) - } else { - Text(" ") - } - } - .frame(width: 45, height: 45) - .background { - let status = (isKeyboardShowing && otpText.count == index) - RoundedRectangle(cornerRadius: 6, style: .continuous) - .stroke(status ? activeIndicatorColor : inactiveIndicatorColor) - .animation(.easeInOut(duration: 0.2), value: status) - - } - .padding() - } -} - -@available(iOS 13.0, *) -extension Binding where Value == String { - func limit(_ length: Int)->Self { - if self.wrappedValue.count > length { - DispatchQueue.main.async { - self.wrappedValue = String(self.wrappedValue.prefix(length)) - } - } - return self - } -} diff --git a/Sources/Web3Modal/Sheets/ModalContainerView.swift b/Sources/Web3Modal/Sheets/ModalContainerView.swift index ce2d652..0143821 100644 --- a/Sources/Web3Modal/Sheets/ModalContainerView.swift +++ b/Sources/Web3Modal/Sheets/ModalContainerView.swift @@ -90,6 +90,12 @@ struct ModalContainerView: View { @available(iOS 14.0, *) struct ModalContainerView_Previews: PreviewProvider { static var previews: some View { - ModalContainerView(router: Router()) + ModalContainerView(router: .mock) + .withMockSetup() +// .onAppear { +// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { +// Store.mock.wallets = [] +// } +// } } } diff --git a/Sources/Web3Modal/Sheets/Web3ModalView.swift b/Sources/Web3Modal/Sheets/Web3ModalView.swift index 0fe2f72..a1ee000 100644 --- a/Sources/Web3Modal/Sheets/Web3ModalView.swift +++ b/Sources/Web3Modal/Sheets/Web3ModalView.swift @@ -23,7 +23,7 @@ struct Web3ModalView: View { EmptyView() case .connectWallet: ConnectWalletView() - case .otp: + case .otpInput: EnterOTPView() case .allWallets: if #available(iOS 14.0, *) { @@ -46,6 +46,12 @@ struct Web3ModalView: View { ) case .getWallet: GetAWalletView() + case let .otpResult(success): + Text("verify OTP \(success ? "success" : "failed")") + case .some(.verifyDevice): + Text("Please verify your device first to continue, by going to the magic link sent to your email.") + case .some(.magicWebview): + Text("Magic webview here 🧚") } } @@ -116,7 +122,7 @@ extension Router.ConnectingSubpage { return "Connect wallet" case .qr: return "WalletConnect" - case .otp: + case .otpInput, .otpResult: return "Confirm Email" case .allWallets: return "All wallets" @@ -126,20 +132,88 @@ extension Router.ConnectingSubpage { return "\(wallet.name)" case .getWallet: return "Get wallet" + case .verifyDevice: + return "Verify Device" + case .magicWebview: + return "Magic webview TBD" } } } struct Web3ModalView_Previews: PreviewProvider { + class MockWebSocketConnecting: WebSocketConnecting { + init() { + self.onConnect = nil + self.onDisconnect = nil + self.onText = nil + self.request = URLRequest(url: URL(string: "www.google.com")!) + } + + var isConnected: Bool { false } + var onConnect: (() -> Void)? + var onDisconnect: ((Error?) -> Void)? + var onText: ((String) -> Void)? + var request: URLRequest + func connect() {} + func disconnect() {} + func write(string: String, completion: (() -> Void)?) {} + } + + struct MockSockFactory: WebSocketFactory { + public init() {} + + public func create(with url: URL) -> WebSocketConnecting { + return MockWebSocketConnecting() + } + } + + static let viewModel: Web3ModalViewModel = { + let projectId = Bundle.main.object(forInfoDictionaryKey: "PROJECT_ID") as? String ?? "" + + let metadata = AppMetadata( + name: "Web3Modal Swift Dapp", + description: "Web3Modal DApp sample", + url: "www.web3modal.com", + icons: ["https://avatars.githubusercontent.com/u/37784886"], + redirect: .init(native: "w3mdapp://", universal: nil) + ) + + Networking.configure( + groupIdentifier: "group.com.walletconnect.web3modal", + projectId: projectId, + socketFactory: MockSockFactory() + ) + + Web3Modal.configure( + projectId: projectId, + metadata: metadata + ) + + return .init( + router: router, + store: store, + w3mApiInteractor: W3MAPIInteractor(store: store), + signInteractor: SignInteractor(store: store), + blockchainApiInteractor: BlockchainAPIInteractor(store: store) + ) + }() + + static let router = { + let router = Router() + router.setRoute(Router.ConnectingSubpage.otpInput) + return router + }() + + static let store = { + let store = Store() + store.email = "radek@walletconnect.com" + return store + }() + static var previews: some View { - Web3ModalView( - viewModel: .init( - router: Router(), - store: Store(), - w3mApiInteractor: W3MAPIInteractor(store: Store()), - signInteractor: SignInteractor(store: Store()), - blockchainApiInteractor: BlockchainAPIInteractor(store: Store()) - )) + Web3ModalView(viewModel: viewModel) + .environmentObject(router) + .environmentObject(store) .previewLayout(.sizeThatFits) } } diff --git a/Sources/Web3Modal/Store.swift b/Sources/Web3Modal/Store.swift index 67c279f..644cd54 100644 --- a/Sources/Web3Modal/Store.swift +++ b/Sources/Web3Modal/Store.swift @@ -67,22 +67,8 @@ class Store: ObservableObject { @Published var toast: Toast? = nil - // Magic specific - var magicSession: MagicSession? -} - -struct MagicSession: Codable { - let pk: String - let jwt: String - let rt: String - let userData: MagicUserData - - struct MagicUserData: Codable { - let email: String - let address: String - let chainId: Int - } + var email: String? } struct W3MAccount: Codable { From 6100caf3390e460156a8ae701d32193f84562825 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Tue, 6 Feb 2024 11:36:40 +0100 Subject: [PATCH 6/6] Better OTP handling, basic chain switching; safeArea handling; --- Sample/Example/ContentView.swift | 2 +- .../Web3Modal/Core/MagicMessageHandler.swift | 76 +++++++++++++--- Sources/Web3Modal/Core/MagicService.swift | 66 +++++++------- Sources/Web3Modal/Core/Web3Modal.swift | 2 +- Sources/Web3Modal/Core/Web3ModalClient.swift | 6 ++ .../Web3Modal/Helpers/PreviewSupport.swift | 13 ++- Sources/Web3Modal/Router.swift | 1 - Sources/Web3Modal/Screens/AccountView.swift | 1 - .../Screens/ChainSwitch/ChainSelectView.swift | 1 - .../NetworkDetail/NetworkDetailView.swift | 1 - .../NetworkDetailViewModel.swift | 6 ++ .../ChainSwitch/WhatIsNetworkView.swift | 2 +- .../ConnectWallet/AllWalletsView.swift | 2 +- .../ConnectWallet/ConnectWalletView.swift | 1 - .../ConnectWallet/ConnectWithQRCode.swift | 1 - .../Screens/ConnectWallet/EnterOTPView.swift | 22 ++++- .../ConnectWallet/GetAWalletView.swift | 1 - .../WalletDetail/WalletDetailView.swift | 1 - .../ConnectWallet/WhatIsWalletView.swift | 2 +- .../Web3Modal/Sheets/ModalContainerView.swift | 15 ++-- Sources/Web3Modal/Sheets/Web3ModalView.swift | 90 ++++--------------- Sources/Web3Modal/Store.swift | 11 ++- .../Miscellaneous/DrawingProgressView.swift | 4 + 23 files changed, 182 insertions(+), 145 deletions(-) diff --git a/Sample/Example/ContentView.swift b/Sample/Example/ContentView.swift index 21480ee..5778f6b 100644 --- a/Sample/Example/ContentView.swift +++ b/Sample/Example/ContentView.swift @@ -41,7 +41,7 @@ struct ContentView: View { }, alignment: .top ) - .onReceive(Web3Modal.instance.socketConnectionStatusPublisher, perform: { status in + .onReceive(Web3Modal.instance.socketConnectionStatusPublisher.receive(on: DispatchQueue.main), perform: { status in socketConnected = status == .connected print("🧦 \(status)") }) diff --git a/Sources/Web3Modal/Core/MagicMessageHandler.swift b/Sources/Web3Modal/Core/MagicMessageHandler.swift index ed07efd..f4cb54d 100644 --- a/Sources/Web3Modal/Core/MagicMessageHandler.swift +++ b/Sources/Web3Modal/Core/MagicMessageHandler.swift @@ -12,17 +12,18 @@ class MagicMessageHandler: NSObject, WKScriptMessageHandler { } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - - print(message.body) + guard let bodyString = message.body as? String, - bodyString.contains("@w3m-frame"), + bodyString.contains("@w3m-frame") || bodyString.contains("@w3m-app"), let data = bodyString.data(using: .utf8) else { return } + print(message.body) + do { let response = try JSONDecoder().decode(MagicMessage.self, from: data) handleMagicResponse(response) @@ -31,13 +32,9 @@ class MagicMessageHandler: NSObject, WKScriptMessageHandler { } } - struct Payload: Codable { - let action: String? - } func handleMagicResponse(_ response: MagicMessage) { - - let payload = try? response.payload?.get(Payload.self) + switch MagicMessage.MessageType(rawValue: response.type) { case .syncThemeSuccess: @@ -45,6 +42,13 @@ class MagicMessageHandler: NSObject, WKScriptMessageHandler { case .syncDataSuccess: break case .connectEmailSuccess: + + struct Payload: Codable { + let action: String? + } + + let payload = try? response.payload?.get(Payload.self) + switch payload?.action { case "VERIFY_OTP": router.setRoute(Router.ConnectingSubpage.otpInput) @@ -56,21 +60,65 @@ class MagicMessageHandler: NSObject, WKScriptMessageHandler { case .connectEmailError: break case .isConnectSuccess: - break + + struct Payload: Codable { + let isConnected: Bool? + } + + let payload = try? response.payload?.get(Payload.self) + + if payload?.isConnected == true { + Web3Modal.magicService.getUser(chainId: store.selectedChain?.id) + } case .isConnectError: - break + + print("No magic connection") case .connectOtpSuccess: - router.setRoute(Router.ConnectingSubpage.otpResult(true)) + store.otpState = .success + + Web3Modal.magicService.getUser(chainId: store.selectedChain?.id) case .connectOtpError: - router.setRoute(Router.ConnectingSubpage.otpResult(false)) + store.otpState = .error case .getUserSuccess: - print("getUserSuccess") + + struct Payload: Codable { + let chainId: Int? + let address: String? + } + + guard + let payload = try? response.payload?.get(Payload.self), + let address = payload.address, + let chainId = payload.chainId, + let blockChain = Blockchain("eip155:\(chainId)") + else { + return + } + + store.account = .init(address: address, chain: blockChain) + store.connectedWith = .magic + router.setRoute(Router.AccountSubpage.profile) case .getUserError: print("getUserError") case .sessionUpdate: break case .switchNetworkSuccess: - break + + struct Payload: Codable { + let chainId: Int? + let address: String? + } + + guard + let payload = try? response.payload?.get(Payload.self), + let chainId = payload.chainId, + let blockChain = Blockchain("eip155:\(chainId)") + else { + return + } + + store.selectedChain = ChainPresets.ethChains.first(where: { $0.id == blockChain.absoluteString }) + router.setRoute(Router.AccountSubpage.profile) case .switchNetworkError: break case .rpcRequestSuccess: diff --git a/Sources/Web3Modal/Core/MagicService.swift b/Sources/Web3Modal/Core/MagicService.swift index 8824ceb..ee8abdb 100644 --- a/Sources/Web3Modal/Core/MagicService.swift +++ b/Sources/Web3Modal/Core/MagicService.swift @@ -7,7 +7,6 @@ enum Web3ModalTheme: String { } class MagicService { - private let injectScript = """ window.addEventListener('message', ({ data }) => { window.webkit.messageHandlers.nativeProcess.postMessage(JSON.stringify(data)) @@ -80,31 +79,31 @@ class MagicService { attachedToViewHierarchy = true } - public func connectEmail(email: String) async { + public func connectEmail(email: String) { let message = MagicRequest.ConnectEmail(email: email).toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } func connectDevice() async { let message = MagicRequest.ConnectDevice().toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } func connectOtp(otp: String) async { // Assuming waitConfirmation is a property you're using to track state // waitConfirmation.value = true let message = MagicRequest.ConnectOtp(otp: otp).toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } - func isConnected() async { + func isConnected() { let message = MagicRequest.IsConnected().toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } func getChainId() async { let message = MagicRequest.GetChainId().toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } // Example of a commented-out function, updateEmail, for reference @@ -112,10 +111,10 @@ class MagicService { // await runJavaScript("provider.updateEmail('\(email)')") // } - func syncTheme(theme: Web3ModalTheme?) async { + func syncTheme(theme: Web3ModalTheme?) { guard let mode = theme?.rawValue else { return } let message = MagicRequest.SyncTheme(mode: mode) - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } func syncDappData( @@ -124,55 +123,60 @@ class MagicService { projectId: String ) async { let message = MagicRequest.SyncAppData(metadata: metadata, sdkVersion: sdkVersion, projectId: projectId).toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } - func getUser(chainId: String?) async { + func getUser(chainId: String?) { let message = MagicRequest.GetUser(chainId: chainId).toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } - func switchNetwork(chainId: String) async { + func switchNetwork(chainId: String) { let message = MagicRequest.SwitchNetwork(chainId: chainId).toString - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") } - func disconnect() async { - let message = "SignOut()" - await runJavaScript("sendMessage(\(message))") + func disconnect() { + let message = MagicRequest.SignOut() + runJavaScript("sendMessage(\(message))") } func request(parameters: [String: Any]) async { guard let method = parameters["method"] as? String, let params = parameters["params"] as? [Any] else { return } let message = "RpcRequest(method: \(method), params: \(params))" - await runJavaScript("sendMessage(\(message))") + runJavaScript("sendMessage(\(message))") // Handle onApproveTransaction if needed } - @MainActor - private func runJavaScript(_ script: String) async { + private func runJavaScript(_ script: String) { if !attachedToViewHierarchy { attachToViewHierarchy() } - do { - let result = try await webview.evaluateJavaScript(""" - setTimeout(() => { - \(script) - }, 100) - """) - } catch { - print("JavaScript execution error: \(error)") + Task { @MainActor in + do { + _ = try await webview.evaluateJavaScript(""" + setTimeout(() => { + \(script) + }, 100) + """) + } catch { + print("JavaScript execution error: \(error)") + } } } } class NavigationDelegate: NSObject, WKNavigationDelegate { - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {} + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Web3Modal.magicService.isConnected() + } + } } -extension UIApplication { +private extension UIApplication { static var keyWindow: UIWindow? { let allScenes = UIApplication.shared.connectedScenes for scene in allScenes { diff --git a/Sources/Web3Modal/Core/Web3Modal.swift b/Sources/Web3Modal/Core/Web3Modal.swift index 673e420..29c20bf 100644 --- a/Sources/Web3Modal/Core/Web3Modal.swift +++ b/Sources/Web3Modal/Core/Web3Modal.swift @@ -81,7 +81,7 @@ public class Web3Modal { private(set) static var viewModel: Web3ModalViewModel! - private(set) static var magicService: MagicService? + private(set) static var magicService: MagicService! private init() {} diff --git a/Sources/Web3Modal/Core/Web3ModalClient.swift b/Sources/Web3Modal/Core/Web3ModalClient.swift index 2d73561..ffa76c9 100644 --- a/Sources/Web3Modal/Core/Web3ModalClient.swift +++ b/Sources/Web3Modal/Core/Web3ModalClient.swift @@ -238,6 +238,10 @@ public class Web3ModalClient { self.coinbaseResponseSubject.send(response) } } + case .magic: + + // TODO: Send request to Magic SDK + assertionFailure() case .none: break } @@ -272,6 +276,8 @@ public class Web3ModalClient { } case .cb: CoinbaseWalletSDK.shared.resetSession() + case .magic: + await Web3Modal.magicService?.disconnect() case .none: break } diff --git a/Sources/Web3Modal/Helpers/PreviewSupport.swift b/Sources/Web3Modal/Helpers/PreviewSupport.swift index 2df9937..a609926 100644 --- a/Sources/Web3Modal/Helpers/PreviewSupport.swift +++ b/Sources/Web3Modal/Helpers/PreviewSupport.swift @@ -49,7 +49,7 @@ extension Store { extension View { - func mockSetup() { + func mockSetup() -> Bool { let projectId = "" // your project_id goes here @@ -74,12 +74,17 @@ extension View { metadata: metadata, store: .mock ) + + return true } + @ViewBuilder func withMockSetup() -> some View { - mockSetup() - - return self + if mockSetup() { + self + } else { + EmptyView() + } } func mockStore(_ mutation: (Store) -> Void) -> some View { diff --git a/Sources/Web3Modal/Router.swift b/Sources/Web3Modal/Router.swift index 542aa54..02ae3ae 100644 --- a/Sources/Web3Modal/Router.swift +++ b/Sources/Web3Modal/Router.swift @@ -56,7 +56,6 @@ class Router: ObservableObject { // Magic case otpInput - case otpResult(Bool) case verifyDevice case magicWebview } diff --git a/Sources/Web3Modal/Screens/AccountView.swift b/Sources/Web3Modal/Screens/AccountView.swift index e062f58..8967dfd 100644 --- a/Sources/Web3Modal/Screens/AccountView.swift +++ b/Sources/Web3Modal/Screens/AccountView.swift @@ -121,7 +121,6 @@ struct AccountView: View { } .padding(.horizontal, Spacing.s) .padding(.top, 40) - .padding(.bottom) .onAppear { Task { do { diff --git a/Sources/Web3Modal/Screens/ChainSwitch/ChainSelectView.swift b/Sources/Web3Modal/Screens/ChainSwitch/ChainSelectView.swift index f159c3a..6cc82a5 100644 --- a/Sources/Web3Modal/Screens/ChainSwitch/ChainSelectView.swift +++ b/Sources/Web3Modal/Screens/ChainSwitch/ChainSelectView.swift @@ -81,7 +81,6 @@ struct ChainSelectView: View { } .padding(.horizontal) .padding(.top, Spacing.xs) - .padding(.bottom, Spacing.xl) } } diff --git a/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailView.swift b/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailView.swift index af43a51..be04071 100644 --- a/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailView.swift +++ b/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailView.swift @@ -24,7 +24,6 @@ struct NetworkDetailView: View { .padding(.bottom, Spacing.l) } .padding(.horizontal) - .padding(.bottom, Spacing.xl * 2) } func chainImage() -> some View { diff --git a/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailViewModel.swift b/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailViewModel.swift index e52911d..02717c2 100644 --- a/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailViewModel.swift +++ b/Sources/Web3Modal/Screens/ChainSwitch/NetworkDetail/NetworkDetailViewModel.swift @@ -150,6 +150,8 @@ final class NetworkDetailViewModel: ObservableObject { ) case .cb: try await Web3Modal.instance.request(.wallet_switchEthereumChain(chainId: to.chainReference)) + case .magic: + Web3Modal.magicService.switchNetwork(chainId: to.chainReference) case .none: break } @@ -192,6 +194,10 @@ final class NetworkDetailViewModel: ObservableObject { ), rpcUrls: addChainParams.rpcUrls )) + case .magic: + + // TODO: Switch chain with magic + assertionFailure() case .none: break } diff --git a/Sources/Web3Modal/Screens/ChainSwitch/WhatIsNetworkView.swift b/Sources/Web3Modal/Screens/ChainSwitch/WhatIsNetworkView.swift index 7107001..53cc7ae 100644 --- a/Sources/Web3Modal/Screens/ChainSwitch/WhatIsNetworkView.swift +++ b/Sources/Web3Modal/Screens/ChainSwitch/WhatIsNetworkView.swift @@ -50,7 +50,7 @@ struct WhatIsNetworkView: View { .buttonStyle(W3MButtonStyle(size: .s)) } .padding(.vertical, Spacing.xxl) - .padding(.horizontal, Spacing.xl) + .padding(.top, Spacing.xl) } func sections() -> [HelpSection] { diff --git a/Sources/Web3Modal/Screens/ConnectWallet/AllWalletsView.swift b/Sources/Web3Modal/Screens/ConnectWallet/AllWalletsView.swift index ff7b09e..5f9c752 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/AllWalletsView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/AllWalletsView.swift @@ -100,7 +100,7 @@ struct AllWalletsView: View { } } .padding(.horizontal, 12) - .padding(.bottom, 60) + .padding(.bottom, 30) } } diff --git a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift index 015e70c..d2ff026 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWalletView.swift @@ -52,7 +52,6 @@ struct ConnectWalletView: View { )) } .padding(Spacing.s) - .padding(.bottom) } @ViewBuilder diff --git a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWithQRCode.swift b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWithQRCode.swift index 44f6c74..6ca8f9b 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/ConnectWithQRCode.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/ConnectWithQRCode.swift @@ -34,7 +34,6 @@ struct ConnectWithQRCode: View { } .padding([.top, .horizontal], Spacing.xl) .padding(.bottom, Spacing.l) - .padding(.bottom) .background(Color.Background125) .onAppear { connect() diff --git a/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift b/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift index 0965d57..23871f0 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/EnterOTPView.swift @@ -19,10 +19,28 @@ struct EnterOTPView: View { Spacer().frame(height: Spacing.l) - otpInput() + if store.otpState == .loading { + DrawingProgressView( + shape: .circle, + color: .Blue100, + lineWidth: 2, + duration: 1, + isAnimating: .constant(true) + ) + .frame(width: 32, height: 32) + } else { + otpInput() + } Spacer().frame(height: Spacing.s) + if store.otpState == .error { + + Text("Invalid code") + .font(.small400) + .foregroundColor(.Error100) + } + HStack(spacing: Spacing.xxs) { Text("Didn't receive it?") .font(.small400) @@ -98,7 +116,7 @@ public struct OtpView: View { TextField("", text: $otpText) .keyboardType(.numberPad) .textContentType(.oneTimeCode) - .frame(width: 1, height: 1) + .frame(width: 100, height: 20) .opacity(0.001) .blendMode(.screen) .focused($isKeyboardShowing) diff --git a/Sources/Web3Modal/Screens/ConnectWallet/GetAWalletView.swift b/Sources/Web3Modal/Screens/ConnectWallet/GetAWalletView.swift index 085a072..a06840c 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/GetAWalletView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/GetAWalletView.swift @@ -58,7 +58,6 @@ struct GetAWalletView: View { } } .padding(Spacing.s) - .padding(.bottom) } } diff --git a/Sources/Web3Modal/Screens/ConnectWallet/WalletDetail/WalletDetailView.swift b/Sources/Web3Modal/Screens/ConnectWallet/WalletDetail/WalletDetailView.swift index 3ae905b..c8d0963 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/WalletDetail/WalletDetailView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/WalletDetail/WalletDetailView.swift @@ -38,7 +38,6 @@ struct WalletDetailView: View { } } .padding(.horizontal, Spacing.s) - .padding(.bottom, Spacing.xl + 17) } private func picker() -> some View { diff --git a/Sources/Web3Modal/Screens/ConnectWallet/WhatIsWalletView.swift b/Sources/Web3Modal/Screens/ConnectWallet/WhatIsWalletView.swift index fce3953..1a47831 100644 --- a/Sources/Web3Modal/Screens/ConnectWallet/WhatIsWalletView.swift +++ b/Sources/Web3Modal/Screens/ConnectWallet/WhatIsWalletView.swift @@ -51,7 +51,7 @@ struct WhatIsWalletView: View { } .buttonStyle(W3MButtonStyle(size: .s)) } - .padding(.vertical, 40) + .padding(.top, 40) .padding(.horizontal, 40) } diff --git a/Sources/Web3Modal/Sheets/ModalContainerView.swift b/Sources/Web3Modal/Sheets/ModalContainerView.swift index 0143821..76fa956 100644 --- a/Sources/Web3Modal/Sheets/ModalContainerView.swift +++ b/Sources/Web3Modal/Sheets/ModalContainerView.swift @@ -30,6 +30,7 @@ struct ModalContainerView: View { } #endif } + .edgesIgnoringSafeArea(.top) VStack(spacing: 0) { Spacer() @@ -52,6 +53,14 @@ struct ModalContainerView: View { EmptyView() } } + .background( + VStack { + Color.clear + + Color.Background125 + .edgesIgnoringSafeArea([.bottom, .horizontal]) + } + ) .toastView(toast: $store.toast) .transition(.move(edge: .bottom)) .animation(.spring(), value: store.isModalShown) @@ -63,7 +72,6 @@ struct ModalContainerView: View { } } } - .edgesIgnoringSafeArea(.all) .backport.onChange(of: store.isModalShown, perform: { newValue in if newValue == false { withAnimation { @@ -92,10 +100,5 @@ struct ModalContainerView_Previews: PreviewProvider { static var previews: some View { ModalContainerView(router: .mock) .withMockSetup() -// .onAppear { -// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { -// Store.mock.wallets = [] -// } -// } } } diff --git a/Sources/Web3Modal/Sheets/Web3ModalView.swift b/Sources/Web3Modal/Sheets/Web3ModalView.swift index a1ee000..2c02f8b 100644 --- a/Sources/Web3Modal/Sheets/Web3ModalView.swift +++ b/Sources/Web3Modal/Sheets/Web3ModalView.swift @@ -46,8 +46,6 @@ struct Web3ModalView: View { ) case .getWallet: GetAWalletView() - case let .otpResult(success): - Text("verify OTP \(success ? "success" : "failed")") case .some(.verifyDevice): Text("Please verify your device first to continue, by going to the magic link sent to your email.") case .some(.magicWebview): @@ -122,7 +120,7 @@ extension Router.ConnectingSubpage { return "Connect wallet" case .qr: return "WalletConnect" - case .otpInput, .otpResult: + case .otpInput: return "Confirm Email" case .allWallets: return "All wallets" @@ -141,79 +139,23 @@ extension Router.ConnectingSubpage { } struct Web3ModalView_Previews: PreviewProvider { - class MockWebSocketConnecting: WebSocketConnecting { - init() { - self.onConnect = nil - self.onDisconnect = nil - self.onText = nil - self.request = URLRequest(url: URL(string: "www.google.com")!) + static var previews: some View { + Group { + Web3ModalView(viewModel: .init( + router: .mockWith(currentRoute: Router.ConnectingSubpage.otpInput), + store: .mock, + w3mApiInteractor: W3MAPIInteractor(store: .mock), + signInteractor: SignInteractor(store: .mock), + blockchainApiInteractor: BlockchainAPIInteractor(store: .mock) + )) + + .environmentObject(Store.mock) + .environmentObject(Router.mock) + .previewLayout(.sizeThatFits) } - - var isConnected: Bool { false } - var onConnect: (() -> Void)? - var onDisconnect: ((Error?) -> Void)? - var onText: ((String) -> Void)? - var request: URLRequest - func connect() {} - func disconnect() {} - func write(string: String, completion: (() -> Void)?) {} - } - - struct MockSockFactory: WebSocketFactory { - public init() {} - - public func create(with url: URL) -> WebSocketConnecting { - return MockWebSocketConnecting() + .onAppear { + } - } - - static let viewModel: Web3ModalViewModel = { - let projectId = Bundle.main.object(forInfoDictionaryKey: "PROJECT_ID") as? String ?? "" - - let metadata = AppMetadata( - name: "Web3Modal Swift Dapp", - description: "Web3Modal DApp sample", - url: "www.web3modal.com", - icons: ["https://avatars.githubusercontent.com/u/37784886"], - redirect: .init(native: "w3mdapp://", universal: nil) - ) - - Networking.configure( - groupIdentifier: "group.com.walletconnect.web3modal", - projectId: projectId, - socketFactory: MockSockFactory() - ) - - Web3Modal.configure( - projectId: projectId, - metadata: metadata - ) - return .init( - router: router, - store: store, - w3mApiInteractor: W3MAPIInteractor(store: store), - signInteractor: SignInteractor(store: store), - blockchainApiInteractor: BlockchainAPIInteractor(store: store) - ) - }() - - static let router = { - let router = Router() - router.setRoute(Router.ConnectingSubpage.otpInput) - return router - }() - - static let store = { - let store = Store() - store.email = "radek@walletconnect.com" - return store - }() - - static var previews: some View { - Web3ModalView(viewModel: viewModel) - .environmentObject(router) - .environmentObject(store) - .previewLayout(.sizeThatFits) } } diff --git a/Sources/Web3Modal/Store.swift b/Sources/Web3Modal/Store.swift index 644cd54..1acf17e 100644 --- a/Sources/Web3Modal/Store.swift +++ b/Sources/Web3Modal/Store.swift @@ -4,6 +4,7 @@ import SwiftUI enum ConnectionProviderType { case wc case cb + case magic } class Store: ObservableObject { @@ -68,7 +69,15 @@ class Store: ObservableObject { @Published var toast: Toast? = nil // Magic specific - var email: String? + @Published var email: String? + @Published var otpState: OTPState = .input + + enum OTPState { + case input + case success + case loading + case error + } } struct W3MAccount: Codable { diff --git a/Sources/Web3ModalUI/Miscellaneous/DrawingProgressView.swift b/Sources/Web3ModalUI/Miscellaneous/DrawingProgressView.swift index a8a192e..f677590 100644 --- a/Sources/Web3ModalUI/Miscellaneous/DrawingProgressView.swift +++ b/Sources/Web3ModalUI/Miscellaneous/DrawingProgressView.swift @@ -247,6 +247,10 @@ class ProgressShapeLayer: CAShapeLayer { self.lineCap = .round } + override init(layer: Any) { + super.init(layer: layer) + } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented")