diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index b7c0db4b4..8b3747cf1 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { name: "Swift Dapp", description: "a description", url: "wallet.connect", - icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) + icons: ["https://avatars.githubusercontent.com/u/37784886"]) Auth.configure(Auth.Config(metadata: metadata, projectId: "8ba9ee138960775e5231b70cc5ef1c3a")) Auth.instance.sessionDeletePublisher .receive(on: DispatchQueue.main) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index e2b5b79fe..0748aaa9b 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -29,8 +29,6 @@ 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84494387278D9C1B00CC26BB /* UIAlertController.swift */; }; 844943A1278EC49700CC26BB /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = 844943A0278EC49700CC26BB /* Web3 */; }; 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFB274F98A10081F94C /* RequestViewController.swift */; }; - 8460DD002750D6F50081F94C /* SessionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFF2750D6F50081F94C /* SessionDetailsViewController.swift */; }; - 8460DD022750D7020081F94C /* SessionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DD012750D7020081F94C /* SessionDetailsView.swift */; }; 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE641E27981DED00142511 /* AppDelegate.swift */; }; 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE642027981DED00142511 /* SceneDelegate.swift */; }; 84CE642827981DF000142511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642727981DF000142511 /* Assets.xcassets */; }; @@ -49,6 +47,11 @@ 84CE647027A2CD6B00142511 /* WalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE646F27A2CD6B00142511 /* WalletTests.swift */; }; 84F568C2279582D200D0A289 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C1279582D200D0A289 /* Signer.swift */; }; 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; + A5A4FC56283CBB7800BBEC1E /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */; }; + A5A4FC58283CBB9F00BBEC1E /* SessionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */; }; + A5A4FC5A283CC08600BBEC1E /* SessionNamespaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */; }; + A5A4FC5C283D1F6700BBEC1E /* SessionDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC5B283D1F6700BBEC1E /* SessionDetailViewController.swift */; }; + A5A4FC5E283D23CA00BBEC1E /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC5D283D23CA00BBEC1E /* Array.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -102,8 +105,6 @@ 76B6E39E2807A3B6004DF775 /* WalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletViewController.swift; sourceTree = ""; }; 84494387278D9C1B00CC26BB /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; 8460DCFB274F98A10081F94C /* RequestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestViewController.swift; sourceTree = ""; }; - 8460DCFF2750D6F50081F94C /* SessionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailsViewController.swift; sourceTree = ""; }; - 8460DD012750D7020081F94C /* SessionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailsView.swift; sourceTree = ""; }; 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 84CE641E27981DED00142511 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 84CE642027981DED00142511 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -124,6 +125,11 @@ 84CE646F27A2CD6B00142511 /* WalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTests.swift; sourceTree = ""; }; 84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; 84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = ""; }; + A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = ""; }; + A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewModel.swift; sourceTree = ""; }; + A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNamespaceViewModel.swift; sourceTree = ""; }; + A5A4FC5B283D1F6700BBEC1E /* SessionDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewController.swift; sourceTree = ""; }; + A5A4FC5D283D23CA00BBEC1E /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -179,6 +185,7 @@ children = ( 761C64A526FCB0AA004239D1 /* SessionInfo.swift */, 84F568C32795832A00D0A289 /* EthereumTransaction.swift */, + A5A4FC5D283D23CA00BBEC1E /* Array.swift */, 84F568C1279582D200D0A289 /* Signer.swift */, 765056262821989600F9AE79 /* Color+Extension.swift */, 84494387278D9C1B00CC26BB /* UIAlertController.swift */, @@ -261,8 +268,10 @@ 8460DCFE2750D6DF0081F94C /* SessionDetails */ = { isa = PBXGroup; children = ( - 8460DCFF2750D6F50081F94C /* SessionDetailsViewController.swift */, - 8460DD012750D7020081F94C /* SessionDetailsView.swift */, + A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */, + A5A4FC5B283D1F6700BBEC1E /* SessionDetailViewController.swift */, + A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */, + A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */, ); path = SessionDetails; sourceTree = ""; @@ -512,22 +521,25 @@ 765056272821989600F9AE79 /* Color+Extension.swift in Sources */, 76235E8D28202043004ED0AA /* RequestView.swift in Sources */, 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */, - 8460DD002750D6F50081F94C /* SessionDetailsViewController.swift in Sources */, 76744CF926FE4D7400B77ED9 /* ActiveSessionCell.swift in Sources */, 764E1D4026F8D3FC00A1FB15 /* AppDelegate.swift in Sources */, 761C64A626FCB0AA004239D1 /* SessionInfo.swift in Sources */, + A5A4FC5E283D23CA00BBEC1E /* Array.swift in Sources */, 76235E8B28201C9C004ED0AA /* Utilities.swift in Sources */, 76744CF726FE4D5400B77ED9 /* ActiveSessionItem.swift in Sources */, + A5A4FC5A283CC08600BBEC1E /* SessionNamespaceViewModel.swift in Sources */, 764E1D4226F8D3FC00A1FB15 /* SceneDelegate.swift in Sources */, 84F568C2279582D200D0A289 /* Signer.swift in Sources */, + A5A4FC56283CBB7800BBEC1E /* SessionDetailView.swift in Sources */, 7600223B2819FC0B0011DD38 /* ProposalView.swift in Sources */, 761248172819F9E600CB6D48 /* WalletView.swift in Sources */, - 8460DD022750D7020081F94C /* SessionDetailsView.swift in Sources */, + A5A4FC58283CBB9F00BBEC1E /* SessionDetailViewModel.swift in Sources */, 76B6E39F2807A3B6004DF775 /* WalletViewController.swift in Sources */, 764E1D5A26F8DF1B00A1FB15 /* ScannerViewController.swift in Sources */, 760022392819FBF90011DD38 /* ProposalViewController.swift in Sources */, 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */, 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */, + A5A4FC5C283D1F6700BBEC1E /* SessionDetailViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index bbe9aeaa9..6f994f41f 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -11,7 +11,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { name: "Example Wallet", description: "wallet description", url: "example.wallet", - icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) + icons: ["https://avatars.githubusercontent.com/u/37784886"]) Auth.configure(Auth.Config(metadata: metadata, projectId: "8ba9ee138960775e5231b70cc5ef1c3a")) guard let windowScene = (scene as? UIWindowScene) else { return } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailView.swift b/Example/ExampleApp/SessionDetails/SessionDetailView.swift new file mode 100644 index 000000000..11c8cea25 --- /dev/null +++ b/Example/ExampleApp/SessionDetails/SessionDetailView.swift @@ -0,0 +1,138 @@ +import SwiftUI +import WalletConnectAuth + +struct SessionDetailView: View { + + @ObservedObject var viewModel: SessionDetailViewModel + + var didPressSessionRequest: ((Request) -> Void)? + + var body: some View { + List { + Section { headerView() } + + ForEach(viewModel.chains, id: \.self) { chain in + Section(header: header(chain: chain)) { + if let namespace = viewModel.namespace(for: chain) { + + if namespace.accounts.isNotEmpty { + accountSection(chain: chain, namespace: namespace) + } + + if namespace.methods.isNotEmpty { + methodsSection(chain: chain, namespace: namespace) + } + + if namespace.events.isNotEmpty { + methodsSection(chain: chain, namespace: namespace) + } + } + } + } + + if viewModel.requests.isNotEmpty { + requestsSection() + } + } + .listStyle(.insetGrouped) + } +} + +private extension SessionDetailView { + + func accountSection(chain: String, namespace: SessionNamespaceViewModel) -> some View { + Section(header: headerRow("Accounts")) { + ForEach(namespace.accounts, id: \.self) { account in + plainRow(account.absoluteString) + } + .onDelete { indices in Task { + await viewModel.remove(field: .accounts, at: indices, for: chain) + }} + } + } + + func methodsSection(chain: String, namespace: SessionNamespaceViewModel) -> some View { + Section(header: headerRow("Methods")) { + ForEach(namespace.methods, id: \.self) { method in + plainRow(method) + } + .onDelete { indices in Task { + await viewModel.remove(field: .methods, at: indices, for: chain) + }} + } + } + + func eventsSection(chain: String, namespace: SessionNamespaceViewModel) -> some View { + Section(header: headerRow("Events")) { + ForEach(namespace.events, id: \.self) { event in + plainRow(event) + } + .onDelete { indices in Task { + await viewModel.remove(field: .events, at: indices, for: chain) + }} + } + } + + func requestsSection() -> some View { + Section(header: Text("Pending requests")) { + ForEach(viewModel.requests, id: \.method) { request in + Button(action: { didPressSessionRequest?(request) }) { + plainRow(request.method) + } + } + } + } + + func headerView() -> some View { + VStack(spacing: 12.0) { + AsyncImage(url: viewModel.peerIconURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + } placeholder: { + ProgressView().progressViewStyle(.circular) + } + .frame(width: 64, height: 64) + .frame(maxWidth: .infinity) + + Text(viewModel.peerName) + .font(.headline) + + VStack { + Text(viewModel.peerDescription) + Text(viewModel.peerURL) + } + .font(.footnote) + .foregroundColor(.secondaryLabel) + + Button("Ping") { + viewModel.ping() + } + .buttonStyle(BorderedProminentButtonStyle()) + } + .background(Color(.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + } + + func headerRow(_ text: String) -> some View { + return Text(text) + .font(.footnote) + .foregroundColor(.secondaryLabel) + } + + func plainRow(_ text: String) -> some View { + return Text(text) + .font(.body) + } + + func header(chain: String) -> some View { + HStack { + Text(chain) + Spacer() + Button("Delete") { Task { + await viewModel.remove(field: .chain, for: chain) + }} + } + } +} diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift new file mode 100644 index 000000000..b2e98447f --- /dev/null +++ b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift @@ -0,0 +1,47 @@ +import UIKit +import SwiftUI +import WalletConnectAuth +import WalletConnectUtils + +final class SessionDetailViewController: UIHostingController { + + private let viewModel: SessionDetailViewModel + + init(session: Session, client: Auth) { + self.viewModel = SessionDetailViewModel(session: session, client: client) + super.init(rootView: SessionDetailView(viewModel: viewModel)) + + rootView.didPressSessionRequest = { [weak self] request in + self?.showSessionRequest(request) + } + } + + func reload() { + viewModel.objectWillChange.send() + } + + private func showSessionRequest(_ request: Request) { + let viewController = RequestViewController(request) + viewController.onSign = { [unowned self] in + let result = Signer.signEth(request: request) + let response = JSONRPCResponse(id: request.id, result: result) + Auth.instance.respond(topic: request.topic, response: .response(response)) + reload() + } + viewController.onReject = { [unowned self] in + Auth.instance.respond( + topic: request.topic, + response: .error(JSONRPCErrorResponse( + id: request.id, + error: JSONRPCErrorResponse.Error(code: 0, message: "")) + ) + ) + reload() + } + present(viewController, animated: true) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift new file mode 100644 index 000000000..8af18a759 --- /dev/null +++ b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift @@ -0,0 +1,140 @@ +import Foundation +import Combine +import WalletConnectAuth + +@MainActor +final class SessionDetailViewModel: ObservableObject { + private let session: Session + private let client: Auth + + enum Fields { + case accounts + case methods + case events + case chain + } + + @Published var namespaces: [String: SessionNamespace] + + init(session: Session, client: Auth) { + self.session = session + self.client = client + self.namespaces = session.namespaces + } + + var peerName: String { + session.peer.name + } + var peerDescription: String { + session.peer.description + } + var peerURL: String { + session.peer.url + } + var peerIconURL: URL? { + session.peer.icons.first.flatMap { URL(string: $0) } + } + var chains: [String] { + namespaces.keys.sorted() + } + var requests: [Request] { + client.getPendingRequests(topic: session.topic) + } + + func remove(field: Fields, at indices: IndexSet = [], for chain: String) async { + let backup = namespaces + + do { + switch field { + case .accounts: removeAccounts(at: indices, chain: chain) + case .methods: removeMethods(at: indices, chain: chain) + case .events: removeEvents(at: indices, chain: chain) + case .chain: removeChain(chain) + } + + try await client.update( + topic: session.topic, + namespaces: namespaces + ) + } + catch { + namespaces = backup + print("[RESPONDER] Namespaces update failed with: \(error.localizedDescription)") + } + } + + func ping() { + client.ping(topic: session.topic) { result in + switch result { + case .success: + print("[RESPONDER] Received ping response") + case .failure(let error): + print("[RESPONDER] Ping failed with: \(error.localizedDescription)") + } + } + } + + func namespace(for chain: String) -> SessionNamespaceViewModel? { + namespaces[chain].map { SessionNamespaceViewModel(namespace: $0) } + } +} + +private extension SessionDetailViewModel { + + func removeAccounts(at offsets: IndexSet, chain: String) { + guard let viewModel = namespace(for: chain) else { return } + + namespaces[chain] = SessionNamespace( + accounts: Set(viewModel.accounts.removing(atOffsets: offsets)), + namespace: viewModel.namespace + ) + } + + func removeMethods(at offsets: IndexSet, chain: String) { + guard let viewModel = namespace(for: chain) else { return } + + namespaces[chain] = SessionNamespace( + methods: Set(viewModel.methods.removing(atOffsets: offsets)), + namespace: viewModel.namespace + ) + } + + func removeEvents(at offsets: IndexSet, chain: String) { + guard let viewModel = namespace(for: chain) else { return } + + namespaces[chain] = SessionNamespace( + events: Set(viewModel.events.removing(atOffsets: offsets)), + namespace: viewModel.namespace + ) + } + + func removeChain(_ chain: String) { + namespaces.removeValue(forKey: chain) + } +} + +private extension Array { + + func removing(atOffsets offsets: IndexSet) -> Self { + var array = self + array.remove(atOffsets: offsets) + return array + } +} + +private extension SessionNamespace { + + init( + accounts: Set? = nil, + methods: Set? = nil, + events: Set? = nil, + namespace: SessionNamespace + ) { + self.init( + accounts: accounts ?? namespace.accounts, + methods: methods ?? namespace.methods, + events: events ?? namespace.events, + extensions: namespace.extensions + ) + } +} diff --git a/Example/ExampleApp/SessionDetails/SessionDetailsView.swift b/Example/ExampleApp/SessionDetails/SessionDetailsView.swift deleted file mode 100644 index 2ae3ba07c..000000000 --- a/Example/ExampleApp/SessionDetails/SessionDetailsView.swift +++ /dev/null @@ -1,122 +0,0 @@ -import UIKit -import SwiftUI - -final class SessionDetailsView: UIView { - - let iconView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.backgroundColor = .systemFill - imageView.layer.cornerRadius = 32 - return imageView - }() - - let nameLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: 17.0, weight: .heavy) - return label - }() - - let descriptionLabel: UILabel = { - let label = UILabel() - label.font = UIFont.preferredFont(forTextStyle: .subheadline) - label.textColor = .secondaryLabel - label.numberOfLines = 0 - label.textAlignment = .center - return label - }() - - let urlLabel: UILabel = { - let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 14.0) - label.textColor = .tertiaryLabel - return label - }() - - let headerStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 16 - stackView.alignment = .center - return stackView - }() - - - let pingButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Ping", for: .normal) - button.backgroundColor = .systemBlue - button.tintColor = .white - button.layer.cornerRadius = 8 - return button - }() - - let tableView = UITableView() - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .systemBackground - - addSubview(iconView) - addSubview(headerStackView) - addSubview(tableView) - - headerStackView.addArrangedSubview(nameLabel) - headerStackView.addArrangedSubview(urlLabel) - headerStackView.addArrangedSubview(descriptionLabel) - addSubview(pingButton) - - subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - - NSLayoutConstraint.activate([ - iconView.topAnchor.constraint(equalTo: topAnchor, constant: 64), - iconView.centerXAnchor.constraint(equalTo: centerXAnchor), - iconView.widthAnchor.constraint(equalToConstant: 64), - iconView.heightAnchor.constraint(equalToConstant: 64), - - headerStackView.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 32), - headerStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32), - headerStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32), - - tableView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 0), - tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), - tableView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0), - tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), - - pingButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16), - pingButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), - pingButton.heightAnchor.constraint(equalToConstant: 44), - pingButton.widthAnchor.constraint(equalToConstant: 64), - ]) - } - - func loadImage(at url: String) { - guard let iconURL = URL(string: url) else { return } - DispatchQueue.global().async { - if let imageData = try? Data(contentsOf: iconURL) { - DispatchQueue.main.async { [weak self] in - self?.iconView.image = UIImage(data: imageData) - } - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -struct SessionDetailsView_Previews: PreviewProvider { - - static func makeSessionDetailsView() -> SessionDetailsView { - let view = SessionDetailsView() - view.nameLabel.text = "Example name" - view.descriptionLabel.text = String.loremIpsum - view.urlLabel.text = "example.url" - return view - } - - static var previews: some View { - makeSessionDetailsView().makePreview() - } -} diff --git a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift deleted file mode 100644 index 44af78efb..000000000 --- a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift +++ /dev/null @@ -1,135 +0,0 @@ -import UIKit -import WalletConnectAuth -import WalletConnectUtils - -final class SessionDetailsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - private let sessiondetailsView = { - SessionDetailsView() - }() - private var sessionInfo: SessionInfo - private let session: Session - init(_ session: Session) { - let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic).map{$0.method} - let chains = Array(session.namespaces.values.flatMap { n in n.accounts.map{$0.blockchain.absoluteString}}) - let methods = Array(session.namespaces.values.first?.methods ?? []) // TODO: Rethink how to show this info on example app - self.sessionInfo = SessionInfo(name: session.peer.name, - descriptionText: session.peer.description, - dappURL: session.peer.description, - iconURL: session.peer.icons.first ?? "", - chains: chains, - methods: methods, - pendingRequests: pendingRequests) - self.session = session - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - show(sessionInfo) - super.viewDidLoad() - sessiondetailsView.pingButton.addTarget(self, action: #selector(ping), for: .touchUpInside) - sessiondetailsView.tableView.delegate = self - sessiondetailsView.tableView.dataSource = self - sessiondetailsView.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - } - - override func loadView() { - view = sessiondetailsView - } - - private func show(_ sessionInfo: SessionInfo) { - sessiondetailsView.nameLabel.text = sessionInfo.name - sessiondetailsView.descriptionLabel.text = sessionInfo.descriptionText - sessiondetailsView.urlLabel.text = sessionInfo.dappURL - sessiondetailsView.loadImage(at: sessionInfo.iconURL) - } - - @objc - private func ping() { - Auth.instance.ping(topic: session.topic) { result in - switch result { - case .success(): - print("received ping response") - case .failure(let error): - print(error) - } - } - } - - //MARK: - Table View - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return sessionInfo.chains.count - } else if section == 1 { - return sessionInfo.methods.count - } else { - return sessionInfo.pendingRequests.count - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) - if indexPath.section == 0 { - cell.textLabel?.text = sessionInfo.chains[indexPath.row] - } else if indexPath.section == 1 { - cell.textLabel?.text = sessionInfo.methods[indexPath.row] - } else { - cell.textLabel?.text = sessionInfo.pendingRequests[indexPath.row] - } - return cell - } - - func numberOfSections(in tableView: UITableView) -> Int { - return 3 - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - if section == 0 { - return "Chains" - } else if section == 1 { - return "Methods" - } else { - return "Pending Requests" - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.section == 2 { - let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic) - showSessionRequest(pendingRequests[indexPath.row]) - } - } - - private func showSessionRequest(_ sessionRequest: Request) { - let requestVC = RequestViewController(sessionRequest) - requestVC.onSign = { [unowned self] in - let result = Signer.signEth(request: sessionRequest) - let response = JSONRPCResponse(id: sessionRequest.id, result: result) - Auth.instance.respond(topic: sessionRequest.topic, response: .response(response)) - reloadTable() - } - requestVC.onReject = { [unowned self] in - Auth.instance.respond(topic: sessionRequest.topic, response: .error(JSONRPCErrorResponse(id: sessionRequest.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) - reloadTable() - } - present(requestVC, animated: true) - } - - func reloadTable() { - let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic).map{$0.method} - let chains = Array(session.namespaces.values.flatMap { n in n.accounts.map{$0.blockchain.absoluteString}}) - let methods = Array(session.namespaces.values.first?.methods ?? []) // TODO: Rethink how to show this info on example app - self.sessionInfo = SessionInfo(name: session.peer.name, - descriptionText: session.peer.description, - dappURL: session.peer.description, - iconURL: session.peer.icons.first ?? "", - chains: chains, - methods: methods, - pendingRequests: pendingRequests) - sessiondetailsView.tableView.reloadData() - } -} diff --git a/Example/ExampleApp/SessionDetails/SessionNamespaceViewModel.swift b/Example/ExampleApp/SessionDetails/SessionNamespaceViewModel.swift new file mode 100644 index 000000000..a7176fd84 --- /dev/null +++ b/Example/ExampleApp/SessionDetails/SessionNamespaceViewModel.swift @@ -0,0 +1,19 @@ +import Foundation +import WalletConnectAuth + +struct SessionNamespaceViewModel { + let namespace: SessionNamespace + + var accounts: [Account] { + namespace.accounts.sorted(by: { + $0.absoluteString < $1.absoluteString + }) + } + var methods: [String] { + namespace.methods.sorted() + } + + var events: [String] { + namespace.events.sorted() + } +} diff --git a/Example/ExampleApp/SessionProposal/Proposal.swift b/Example/ExampleApp/SessionProposal/Proposal.swift index f70e495ed..d7c1436f4 100644 --- a/Example/ExampleApp/SessionProposal/Proposal.swift +++ b/Example/ExampleApp/SessionProposal/Proposal.swift @@ -38,7 +38,7 @@ struct Proposal { proposerName: "Example name", proposerDescription: String.loremIpsum, proposerURL: "example.url", - iconURL: "https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media", + iconURL: "https://avatars.githubusercontent.com/u/37784886", permissions: [ Namespace( chains: ["eip155:1"], diff --git a/Example/ExampleApp/Shared/Array.swift b/Example/ExampleApp/Shared/Array.swift new file mode 100644 index 000000000..f9d4fbca1 --- /dev/null +++ b/Example/ExampleApp/Shared/Array.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Array { + + var isNotEmpty: Bool { + return !isEmpty + } +} diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index 5aa4af28e..4331c095d 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -53,10 +53,10 @@ final class WalletViewController: UIViewController { proposalViewController.delegate = self present(proposalViewController, animated: true) } - - private func showSessionDetailsViewController(_ session: Session) { - let vc = SessionDetailsViewController(session) - navigationController?.pushViewController(vc, animated: true) + + private func showSessionDetails(with session: Session) { + let viewController = SessionDetailViewController(session: session, client: Auth.instance) + navigationController?.present(viewController, animated: true) } private func showSessionRequest(_ sessionRequest: Request) { @@ -76,8 +76,8 @@ final class WalletViewController: UIViewController { } func reloadSessionDetailsIfNeeded() { - if let sessionDetailsViewController = navigationController?.viewControllers.first(where: {$0 is SessionDetailsViewController}) as? SessionDetailsViewController { - sessionDetailsViewController.reloadTable() + if let viewController = navigationController?.presentedViewController as? SessionDetailViewController { + viewController.reload() } } @@ -130,7 +130,7 @@ extension WalletViewController: UITableViewDataSource, UITableViewDelegate { print("did select row \(indexPath)") let itemTopic = sessionItems[indexPath.row].topic if let session = Auth.instance.getSessions().first{$0.topic == itemTopic} { - showSessionDetailsViewController(session) + showSessionDetails(with: session) } } } diff --git a/Sources/WalletConnectAuth/Auth/AuthClient.swift b/Sources/WalletConnectAuth/Auth/AuthClient.swift index 532ab978c..710ea35fd 100644 --- a/Sources/WalletConnectAuth/Auth/AuthClient.swift +++ b/Sources/WalletConnectAuth/Auth/AuthClient.swift @@ -167,10 +167,10 @@ public final class AuthClient { pairingEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) } - /// For the responder to update session methods + /// For the responder to update session namespaces /// - Parameters: /// - topic: Topic of the session that is intended to be updated. - /// - methods: Sets of methods that will replace existing ones. + /// - namespaces: Dictionary of namespaces that will replace existing ones. public func update(topic: String, namespaces: [String: SessionNamespace]) async throws { try await controllerSessionStateMachine.update(topic: topic, namespaces: namespaces) }