diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d6fdc5ce..60fae593d 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "32f641cf24fc7abc1c591a2025e9f2f572648b0f", - "version": "1.7.2" + "revision": "db51c407d3be4a051484a141bf0bff36c43d3b1e", + "version": "1.8.0" } }, { @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", "state": { "branch": null, - "revision": "04bee4ad86d74d4cb4d7101ff826d6e355301ba9", - "version": "8.9.4" + "revision": "14aa6e47b03b820fd2b338728637570b9e969994", + "version": "8.12.0" } }, { diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index 06f5eed3a..00783bd2a 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -158,9 +158,6 @@ final class NotifyTests: XCTestCase { let updateScope: Set = ["alerts"] expectation.assertForOverFulfill = false - try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) - try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) - var didUpdate = false walletNotifyClientA.subscriptionsPublisher .sink { [unowned self] subscriptions in @@ -181,6 +178,9 @@ final class NotifyTests: XCTestCase { } }.store(in: &publishers) + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } @@ -188,8 +188,6 @@ final class NotifyTests: XCTestCase { let subscribeExpectation = expectation(description: "creates notify subscription") let messageExpectation = expectation(description: "receives a notify message") let notifyMessage = NotifyMessage.stub() - try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) - try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) var didNotify = false walletNotifyClientA.subscriptionsPublisher @@ -215,6 +213,9 @@ final class NotifyTests: XCTestCase { } }.store(in: &publishers) + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + wait(for: [subscribeExpectation, messageExpectation], timeout: InputConfig.defaultTimeout) } diff --git a/Example/Shared/DefaultSocketFactory.swift b/Example/Shared/DefaultSocketFactory.swift index 14bb1ed98..e37b98232 100644 --- a/Example/Shared/DefaultSocketFactory.swift +++ b/Example/Shared/DefaultSocketFactory.swift @@ -6,6 +6,9 @@ extension WebSocket: WebSocketConnecting { } struct DefaultSocketFactory: WebSocketFactory { func create(with url: URL) -> WebSocketConnecting { - return WebSocket(url: url) + let socket = WebSocket(url: url) + let queue = DispatchQueue(label: "com.walletconnect.sdk.sockets", attributes: .concurrent) + socket.callbackQueue = queue + return socket } } diff --git a/Example/WalletApp/ApplicationLayer/Application.swift b/Example/WalletApp/ApplicationLayer/Application.swift index 4015be2e3..95e3a6ab8 100644 --- a/Example/WalletApp/ApplicationLayer/Application.swift +++ b/Example/WalletApp/ApplicationLayer/Application.swift @@ -2,7 +2,7 @@ import Foundation import WalletConnectChat final class Application { - var uri: String? + var uri: WalletConnectURI? var requestSent = false lazy var pushRegisterer = PushRegisterer() @@ -11,4 +11,3 @@ final class Application { lazy var messageSigner = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() lazy var configurationService = ConfigurationService() } - diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index 83200ba56..a8ca2992a 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -26,20 +26,22 @@ final class ConfigurationService { Notify.instance.setLogging(level: .debug) + if let clientId = try? Networking.interactor.getClientId() { + LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId) + ProfilingService.instance.setUpProfiling(account: importAccount.account.absoluteString, clientId: clientId) + } + LoggingService.instance.startLogging() + Task { do { try await Notify.instance.register(account: importAccount.account, domain: "com.walletconnect", onSign: importAccount.onSign) } catch { DispatchQueue.main.async { + let logMessage = LogMessage(message: "Push Server registration failed with: \(error.localizedDescription)") + ProfilingService.instance.send(logMessage: logMessage) UIApplication.currentWindow.rootViewController?.showAlert(title: "Register error", error: error) } } } - - if let clientId = try? Networking.interactor.getClientId() { - LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId) - ProfilingService.instance.setUpProfiling(account: importAccount.account.absoluteString, clientId: clientId) - } - LoggingService.instance.startLogging() } } diff --git a/Example/WalletApp/ApplicationLayer/ProfilingService.swift b/Example/WalletApp/ApplicationLayer/ProfilingService.swift index 5fade9146..bfffe219a 100644 --- a/Example/WalletApp/ApplicationLayer/ProfilingService.swift +++ b/Example/WalletApp/ApplicationLayer/ProfilingService.swift @@ -33,6 +33,7 @@ final class ProfilingService { handleLogs(from: Networking.instance.logsPublisher) handleLogs(from: Notify.instance.logsPublisher) + handleLogs(from: Push.instance.logsPublisher) } private func handleLogs(from publisher: AnyPublisher) { diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index f043c84d5..4a98cfa19 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -28,7 +28,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() - app.uri = connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?uri=", with: "") + app.uri = WalletConnectURI(connectionOptions: connectionOptions) app.requestSent = (connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?", with: "") == "requestSent") configurators.configure() @@ -37,11 +37,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first else { return } - let queryParams = context.url.queryParameters + let uri = WalletConnectURI(urlContext: context) - if let uri = queryParams["uri"] as? String { + if let uri { Task { - try await Pair.instance.pair(uri: WalletConnectURI(string: uri)!) + try await Pair.instance.pair(uri: uri) } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift index 6a249cde1..97d40b444 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift @@ -26,20 +26,31 @@ struct AuthRequestView: View { .foregroundColor(.grey8) .font(.system(size: 22, weight: .medium, design: .rounded)) - Text(presenter.request.payload.domain) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.request.payload.domain) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } .padding(.top, 8) + } else { + Text(presenter.request.payload.domain) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } switch presenter.validationStatus { case .unknown: verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) - case .valid: - verifyBadgeView(imageName: "checkmark.seal.fill", title: "Verified domain", color: .blue) - case .invalid: verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) @@ -68,52 +79,21 @@ struct AuthRequestView: View { } } - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) + + } .padding(20) .background(.ultraThinMaterial) @@ -147,13 +127,12 @@ struct AuthRequestView: View { } .padding(.horizontal, 18) .padding(.vertical, 10) - .frame(height: 250) + .frame(height: 150) } .background(Color.whiteBackground) .cornerRadius(20, corners: .allCorners) .padding(.horizontal, 5) .padding(.bottom, 5) - } .background(.thinMaterial) .cornerRadius(25, corners: .allCorners) @@ -199,6 +178,61 @@ struct AuthRequestView: View { .background(color.opacity(0.15)) .cornerRadius(20) } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } } #if DEBUG diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift index 7bb2cb404..b4c661263 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift @@ -49,9 +49,9 @@ struct NotificationsView: View { private func discover() -> some View { return List { ForEach(presenter.listings) { listing in - listingRow(listing: listing) + discoverListRow(listing: listing) .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)) + .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)) .listRowBackground(Color.clear) } } @@ -147,33 +147,22 @@ struct NotificationsView: View { } } - private func listingRow(listing: ListingViewModel) -> some View { - VStack { - HStack(spacing: 10) { + private func discoverListRow(listing: ListingViewModel) -> some View { + VStack(alignment: .leading, spacing: 16) { + HStack { CacheAsyncImage(url: listing.imageUrl) { phase in if let image = phase.image { image .resizable() - .frame(width: 60, height: 60) + .frame(width: 48.0, height: 48.0) .background(Color.grey8.opacity(0.1)) .cornerRadius(30, corners: .allCorners) } else { Color.grey8.opacity(0.1) - .frame(width: 60, height: 60) + .frame(width: 48.0, height: 48.0) .cornerRadius(30, corners: .allCorners) } } - .padding(.leading, 20) - - VStack(alignment: .leading, spacing: 2) { - Text(listing.title) - .foregroundColor(.grey8) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - - Text(listing.subtitle) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .medium, design: .rounded)) - } Spacer() @@ -182,16 +171,34 @@ struct NotificationsView: View { try await presenter.unsubscribe(subscription: subscription) } .foregroundColor(.red) - .padding(16.0) } else { AsyncButton("Subscribe") { try await presenter.subscribe(listing: listing) } .foregroundColor(.primary) - .padding(16.0) } } + + VStack(alignment: .leading, spacing: 2) { + Text(listing.title) + .foregroundColor(.grey8) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + + Text(listing.appDomain ?? .empty) + .foregroundColor(.grey50) + .font(.system(size: 12, weight: .medium, design: .rounded)) + } + + Text(listing.subtitle) + .foregroundColor(.grey50) + .font(.system(size: 14, weight: .regular, design: .rounded)) + .lineLimit(2) } + .padding(16.0) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.grey95, lineWidth: 1) + ) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift index 118cdc674..441c001e0 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift @@ -85,6 +85,7 @@ private extension SubscriptionPresenter { interactor.messagesPublisher .receive(on: DispatchQueue.main) + .debounce(for: 1, scheduler: RunLoop.main) .sink { [weak self] messages in guard let self = self else { return } self.pushMessages = self.interactor.getPushMessages() diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift index 280c29a71..cd595a158 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift @@ -30,20 +30,31 @@ struct SessionProposalView: View { .foregroundColor(.grey8) .font(.system(size: 22, weight: .medium, design: .rounded)) - Text(presenter.sessionProposal.proposer.url) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.sessionProposal.proposer.url) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } .padding(.top, 8) + } else { + Text(presenter.sessionProposal.proposer.url) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } switch presenter.validationStatus { case .unknown: verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) - case .valid: - verifyBadgeView(imageName: "checkmark.seal.fill", title: "Verified domain", color: .blue) - case .invalid: verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) @@ -89,7 +100,7 @@ struct SessionProposalView: View { } } } - .frame(height: 250) + .frame(height: 150) .cornerRadius(20) .padding(.vertical, 12) @@ -107,52 +118,19 @@ struct SessionProposalView: View { EmptyView() } - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) } .padding(20) .background(.ultraThinMaterial) @@ -284,6 +262,61 @@ struct SessionProposalView: View { .background(color.opacity(0.15)) .cornerRadius(20) } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } } #if DEBUG diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift index b3dadc1f9..97816ea69 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift @@ -22,4 +22,8 @@ final class SessionRequestInteractor { response: .error(.init(code: 0, message: "")) ) } + + func getSession(topic: String) -> Session? { + return Web3Wallet.instance.getSessions().first(where: { $0.topic == topic }) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 59099c781..2cd2fc2d8 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -9,10 +9,13 @@ final class SessionRequestPresenter: ObservableObject { private let importAccount: ImportAccount let sessionRequest: Request + let session: Session? let validationStatus: VerifyContext.ValidationStatus? var message: String { - return String(describing: sessionRequest.params.value) + let message = try? sessionRequest.params.get([String].self) + let decryptedMessage = message.map { String(data: Data(hex: $0.first ?? ""), encoding: .utf8) } + return (decryptedMessage ?? "Failed to decrypt") ?? "Failed to decrypt" } @Published var showError = false @@ -31,6 +34,7 @@ final class SessionRequestPresenter: ObservableObject { self.interactor = interactor self.router = router self.sessionRequest = sessionRequest + self.session = interactor.getSession(topic: sessionRequest.topic) self.importAccount = importAccount self.validationStatus = context?.validation } @@ -55,9 +59,7 @@ final class SessionRequestPresenter: ObservableObject { // MARK: - Private functions private extension SessionRequestPresenter { - func setupInitialState() { - - } + func setupInitialState() {} } // MARK: - SceneViewModel diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift index 92da9ada7..c8ce70396 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift @@ -22,13 +22,31 @@ struct SessionRequestView: View { .font(.system(size: 22, weight: .bold, design: .rounded)) .padding(.top, 10) + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.session?.peer.url ?? "") + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } + .padding(.top, 8) + } else { + Text(presenter.session?.peer.url ?? "") + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } + switch presenter.validationStatus { case .unknown: verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) - case .valid: - verifyBadgeView(imageName: "checkmark.seal.fill", title: "Verified domain", color: .blue) - case .invalid: verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) @@ -57,52 +75,19 @@ struct SessionRequestView: View { EmptyView() } - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) } .padding(20) .background(.ultraThinMaterial) @@ -136,10 +121,11 @@ struct SessionRequestView: View { Text(presenter.message) .foregroundColor(.grey50) .font(.system(size: 13, weight: .semibold, design: .rounded)) + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 18) .padding(.vertical, 10) - .frame(height: 250) + .frame(height: 150) } .background(Color.whiteBackground) .cornerRadius(20, corners: .allCorners) @@ -191,6 +177,61 @@ struct SessionRequestView: View { .background(color.opacity(0.15)) .cornerRadius(20) } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } } #if DEBUG diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index c8a5baee1..5a1bc3b85 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -120,12 +120,10 @@ extension WalletPresenter { } private func pairFromDapp() { - guard let uri = app.uri, - let walletConnectUri = WalletConnectURI(string: uri) - else { + guard let uri = app.uri else { return } - pair(uri: walletConnectUri) + pair(uri: uri) } private func removePairingIndicator() { diff --git a/Sources/WalletConnectKMS/Keychain/KeychainError.swift b/Sources/WalletConnectKMS/Keychain/KeychainError.swift index 37974619c..78c16dcf7 100644 --- a/Sources/WalletConnectKMS/Keychain/KeychainError.swift +++ b/Sources/WalletConnectKMS/Keychain/KeychainError.swift @@ -1,13 +1,17 @@ import Foundation // TODO: Integrate with WalletConnectError -struct KeychainError: Error { +struct KeychainError: Error, LocalizedError { let status: OSStatus init(_ status: OSStatus) { self.status = status } + + var errorDescription: String? { + return "OSStatus: \(status), message: \(status.message)" + } } extension KeychainError: CustomStringConvertible { diff --git a/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift b/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift new file mode 100644 index 000000000..7fe700216 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine + +protocol NotificationPublishing { + func publisher(for name: NSNotification.Name) -> AnyPublisher +} + +extension NotificationCenter: NotificationPublishing { + func publisher(for name: NSNotification.Name) -> AnyPublisher { + return publisher(for: name, object: nil).eraseToAnyPublisher() + } +} + +#if DEBUG +class MockNotificationCenter: NotificationPublishing { + private let subject = PassthroughSubject() + + func publisher(for name: NSNotification.Name) -> AnyPublisher { + return subject.eraseToAnyPublisher() + } + + func post(name: NSNotification.Name) { + subject.send(Notification(name: name)) + } +} +#endif + diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index e8f5834cb..31da26d27 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -14,8 +14,7 @@ public class NotifyClient { } public var logsPublisher: AnyPublisher { - logger.logsPublisher - .eraseToAnyPublisher() + return logger.logsPublisher } private let deleteNotifySubscriptionRequester: DeleteNotifySubscriptionRequester @@ -32,9 +31,9 @@ public class NotifyClient { private let notifyUpdateRequester: NotifyUpdateRequester private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater - private let notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequester private let notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber private let notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber + private let subscriptionWatcher: SubscriptionWatcher init(logger: ConsoleLogging, kms: KeyManagementServiceProtocol, @@ -49,9 +48,9 @@ public class NotifyClient { notifyUpdateRequester: NotifyUpdateRequester, notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, subscriptionsAutoUpdater: SubscriptionsAutoUpdater, - notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequester, notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber, - notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber + notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber, + subscriptionWatcher: SubscriptionWatcher ) { self.logger = logger self.pushClient = pushClient @@ -65,14 +64,14 @@ public class NotifyClient { self.notifyUpdateRequester = notifyUpdateRequester self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber self.subscriptionsAutoUpdater = subscriptionsAutoUpdater - self.notifyWatchSubscriptionsRequester = notifyWatchSubscriptionsRequester self.notifyWatchSubscriptionsResponseSubscriber = notifyWatchSubscriptionsResponseSubscriber self.notifySubscriptionsChangedRequestSubscriber = notifySubscriptionsChangedRequestSubscriber + self.subscriptionWatcher = subscriptionWatcher } public func register(account: Account, domain: String, isLimited: Bool = false, onSign: @escaping SigningCallback) async throws { try await identityService.register(account: account, domain: domain, isLimited: isLimited, onSign: onSign) - notifyWatchSubscriptionsRequester.setAccount(account) + subscriptionWatcher.setAccount(account) } public func setLogging(level: LoggingLevel) { @@ -80,7 +79,24 @@ public class NotifyClient { } public func subscribe(appDomain: String, account: Account) async throws { - try await notifySubscribeRequester.subscribe(appDomain: appDomain, account: account) + return try await withCheckedThrowingContinuation { continuation in + + var cancellable: AnyCancellable? + cancellable = subscriptionsPublisher.sink { subscriptions in + guard subscriptions.contains(where: { $0.metadata.url == appDomain }) else { return } + cancellable?.cancel() + continuation.resume(with: .success(())) + } + + Task { [cancellable] in + do { + try await notifySubscribeRequester.subscribe(appDomain: appDomain, account: account) + } catch { + cancellable?.cancel() + continuation.resume(throwing: error) + } + } + } } public func update(topic: String, scope: Set) async throws { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift index 500d86291..5f7d3ec22 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -62,6 +62,7 @@ public struct NotifyClientFactory { let notifySubscriptionsBuilder = NotifySubscriptionsBuilder(notifyConfigProvider: notifyConfigProvider) let notifyWatchSubscriptionsResponseSubscriber = NotifyWatchSubscriptionsResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage, groupKeychainStorage: groupKeychainStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) let notifySubscriptionsChangedRequestSubscriber = NotifySubscriptionsChangedRequestSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, kms: kms, identityClient: identityClient, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) + let subscriptionWatcher = SubscriptionWatcher(notifyWatchSubscriptionsRequester: notifyWatchSubscriptionsRequester, logger: logger) let identityService = NotifyIdentityService(keyserverURL: keyserverURL, identityClient: identityClient, logger: logger) @@ -79,9 +80,9 @@ public struct NotifyClientFactory { notifyUpdateRequester: notifyUpdateRequester, notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, subscriptionsAutoUpdater: subscriptionsAutoUpdater, - notifyWatchSubscriptionsRequester: notifyWatchSubscriptionsRequester, notifyWatchSubscriptionsResponseSubscriber: notifyWatchSubscriptionsResponseSubscriber, - notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber + notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber, + subscriptionWatcher: subscriptionWatcher ) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift index ab65a9e0c..3d90c7d56 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift @@ -1,7 +1,12 @@ import Foundation import Combine -class NotifyWatchSubscriptionsRequester { +protocol NotifyWatchSubscriptionsRequesting { + func setAccount(_ account: Account) + func watchSubscriptions() async throws +} + +class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { private let keyserverURL: URL private let identityClient: IdentityClient @@ -28,24 +33,13 @@ class NotifyWatchSubscriptionsRequester { self.kms = kms self.webDidResolver = webDidResolver self.notifyHost = notifyHost - setUpWatchSubscriptionsOnSocketConnection() - } - - func setUpWatchSubscriptionsOnSocketConnection() { - networkingInteractor.socketConnectionStatusPublisher - .sink { [unowned self] status in - guard status == .connected else { return } - Task { try await watchSubscriptions() } - } - .store(in: &publishers) } func setAccount(_ account: Account) { self.account = account - Task { try await watchSubscriptions() } } - private func watchSubscriptions() async throws { + func watchSubscriptions() async throws { guard let account = account else { return } @@ -113,3 +107,15 @@ class NotifyWatchSubscriptionsRequester { ) } } + +#if DEBUG +class MockNotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { + func setAccount(_ account: WalletConnectUtils.Account) {} + + var onWatchSubscriptions: (() -> Void)? + + func watchSubscriptions() async throws { + onWatchSubscriptions?() + } +} +#endif diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift new file mode 100644 index 000000000..a98d70a05 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift @@ -0,0 +1,74 @@ +import Foundation +import Combine +#if os(iOS) +import UIKit +#endif + +class SubscriptionWatcher { + + private var timerCancellable: AnyCancellable? + private var appLifecycleCancellable: AnyCancellable? + private var notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting + private let logger: ConsoleLogging + private let backgroundQueue = DispatchQueue(label: "com.walletconnect.subscriptionWatcher", qos: .background) + private let notificationCenter: NotificationPublishing + private var watchSubscriptionsWorkItem: DispatchWorkItem? + + var timerInterval: TimeInterval = 5 * 60 + var debounceInterval: TimeInterval = 0.5 + var onSetupTimer: (() -> Void)? + + init(notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting, + logger: ConsoleLogging, + notificationCenter: NotificationPublishing = NotificationCenter.default) { + self.notifyWatchSubscriptionsRequester = notifyWatchSubscriptionsRequester + self.logger = logger + self.notificationCenter = notificationCenter + } + + func setupTimer() { + onSetupTimer?() + logger.debug("Setting up Subscription Watcher timer") + timerCancellable?.cancel() + timerCancellable = Timer.publish(every: timerInterval, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.backgroundQueue.async { + self?.watchSubscriptions() + } + } + } + + func setAccount(_ account: Account) { + notifyWatchSubscriptionsRequester.setAccount(account) + setupTimer() + watchAppLifecycle() + watchSubscriptions() + } + + func watchSubscriptions() { + watchSubscriptionsWorkItem?.cancel() + + let workItem = DispatchWorkItem { [weak self] in + self?.logger.debug("Will watch subscriptions") + Task(priority: .background) { [weak self] in try await self?.notifyWatchSubscriptionsRequester.watchSubscriptions() } + } + + watchSubscriptionsWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + debounceInterval, execute: workItem) + } + + func watchAppLifecycle() { +#if os(iOS) + appLifecycleCancellable = notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.logger.debug("Will setup Subscription Watcher after app entered foreground") + self?.setupTimer() + self?.backgroundQueue.async { + self?.watchSubscriptions() + } + } +#endif + } +} diff --git a/Sources/WalletConnectPush/PushClient.swift b/Sources/WalletConnectPush/PushClient.swift index 2eb23aacd..30f87f9f3 100644 --- a/Sources/WalletConnectPush/PushClient.swift +++ b/Sources/WalletConnectPush/PushClient.swift @@ -1,10 +1,17 @@ import Foundation +import Combine public class PushClient: PushClientProtocol { private let registerService: PushRegisterService + private let logger: ConsoleLogging - init(registerService: PushRegisterService) { + public var logsPublisher: AnyPublisher { + return logger.logsPublisher + } + + init(registerService: PushRegisterService, logger: ConsoleLogging) { self.registerService = registerService + self.logger = logger } public func register(deviceToken: Data) async throws { diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift index 50a0145f9..65c733886 100644 --- a/Sources/WalletConnectPush/PushClientFactory.swift +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -34,6 +34,6 @@ public struct PushClientFactory { let registerService = PushRegisterService(httpClient: httpClient, projectId: projectId, clientIdStorage: clientIdStorage, pushAuthenticator: pushAuthenticator, logger: logger, environment: environment) - return PushClient(registerService: registerService) + return PushClient(registerService: registerService, logger: logger) } } diff --git a/Sources/WalletConnectPush/Register/PushRegisterService.swift b/Sources/WalletConnectPush/Register/PushRegisterService.swift index 2e142ae26..3c06ce281 100644 --- a/Sources/WalletConnectPush/Register/PushRegisterService.swift +++ b/Sources/WalletConnectPush/Register/PushRegisterService.swift @@ -45,13 +45,15 @@ actor PushRegisterService { guard response.status == .success else { throw Errors.registrationFailed } - logger.debug("Successfully registered at Echo Server") + logger.debug("Successfully registered at Push Server") } catch { if (error as? HTTPError) == .couldNotConnect && !fallback { + logger.debug("Trying fallback") fallback = true await echoHostFallback() try await register(deviceToken: deviceToken) } + logger.debug("Push Server registration error: \(error.localizedDescription)") throw error } } diff --git a/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift index 2055e1813..87a7daada 100644 --- a/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift @@ -1,21 +1,23 @@ import Foundation public protocol ClientIdAuthenticating { - func createAuthToken() throws -> String + func createAuthToken(url: String?) throws -> String } -public struct ClientIdAuthenticator: ClientIdAuthenticating { +public final class ClientIdAuthenticator: ClientIdAuthenticating { private let clientIdStorage: ClientIdStoring - private let url: String + private var url: String public init(clientIdStorage: ClientIdStoring, url: String) { self.clientIdStorage = clientIdStorage self.url = url } - public func createAuthToken() throws -> String { + public func createAuthToken(url: String? = nil) throws -> String { + url.flatMap { self.url = $0 } + let keyPair = try clientIdStorage.getOrCreateKeyPair() - let payload = RelayAuthPayload(subject: getSubject(), audience: url) + let payload = RelayAuthPayload(subject: getSubject(), audience: self.url) return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString } diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index d4136fe89..9c34e0b9a 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.8.4"} +{"version": "1.8.5"} diff --git a/Sources/WalletConnectRelay/RelayURLFactory.swift b/Sources/WalletConnectRelay/RelayURLFactory.swift index a7cbecab5..ff99759c0 100644 --- a/Sources/WalletConnectRelay/RelayURLFactory.swift +++ b/Sources/WalletConnectRelay/RelayURLFactory.swift @@ -23,7 +23,7 @@ struct RelayUrlFactory { URLQueryItem(name: "projectId", value: projectId) ] do { - let authToken = try socketAuthenticator.createAuthToken() + let authToken = try socketAuthenticator.createAuthToken(url: fallback ? "wss://" + NetworkConstants.fallbackUrl : "wss://" + relayHost) components.queryItems?.append(URLQueryItem(name: "auth", value: authToken)) } catch { // TODO: Handle token creation errors diff --git a/Sources/WalletConnectUtils/Logger/Log.swift b/Sources/WalletConnectUtils/Logger/Log.swift index 5e3c7f785..8a3bda077 100644 --- a/Sources/WalletConnectUtils/Logger/Log.swift +++ b/Sources/WalletConnectUtils/Logger/Log.swift @@ -1,21 +1,28 @@ import Foundation public struct LogMessage { + public let message: String public let properties: [String: String]? - public var aggregated: String { - var aggregatedProperties = "" - properties?.forEach { key, value in - aggregatedProperties += "\(key): \(value), " - } + public var aggregated: String { + var aggregatedProperties = "" - if !aggregatedProperties.isEmpty { - aggregatedProperties = String(aggregatedProperties.dropLast(2)) - } + properties?.forEach { key, value in + aggregatedProperties += "\(key): \(value), " + } - return "\(message), properties: [\(aggregatedProperties)]" + if !aggregatedProperties.isEmpty { + aggregatedProperties = String(aggregatedProperties.dropLast(2)) } + + return "\(message), properties: [\(aggregatedProperties)]" + } + + public init(message: String, properties: [String : String]? = nil) { + self.message = message + self.properties = properties + } } public enum Log { diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift index 604929ff0..91973f209 100644 --- a/Sources/WalletConnectUtils/WalletConnectURI.swift +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit public struct WalletConnectURI: Equatable { @@ -44,6 +44,27 @@ public struct WalletConnectURI: Equatable { self.symKey = symKey self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) } + + public init?(deeplinkUri: URL) { + if let deeplinkUri = deeplinkUri.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: deeplinkUri) + } + return nil + } + + public init?(connectionOptions: UIScene.ConnectionOptions) { + if let uri = connectionOptions.urlContexts.first?.url.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: uri) + } + return nil + } + + public init?(urlContext: UIOpenURLContext) { + if let uri = urlContext.url.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: uri) + } + return nil + } private var relayQuery: String { var query = "relay-protocol=\(relay.protocol)" diff --git a/Tests/NotifyTests/SubscriptionWatcherTests.swift b/Tests/NotifyTests/SubscriptionWatcherTests.swift new file mode 100644 index 000000000..68549edae --- /dev/null +++ b/Tests/NotifyTests/SubscriptionWatcherTests.swift @@ -0,0 +1,75 @@ +import Foundation +import XCTest +import TestingUtils +@testable import WalletConnectNotify + +class SubscriptionWatcherTests: XCTestCase { + + var sut: SubscriptionWatcher! + var mockRequester: MockNotifyWatchSubscriptionsRequester! + var mockLogger: ConsoleLoggerMock! + var mockNotificationCenter: MockNotificationCenter! + + override func setUp() { + super.setUp() + mockRequester = MockNotifyWatchSubscriptionsRequester() + mockLogger = ConsoleLoggerMock() + mockNotificationCenter = MockNotificationCenter() + sut = SubscriptionWatcher(notifyWatchSubscriptionsRequester: mockRequester, logger: mockLogger, notificationCenter: mockNotificationCenter) + let account = Account("eip155:1:0x1AAe9864337E821f2F86b5D27468C59AA333C877")! + sut.debounceInterval = 0.0001 + sut.setAccount(account) + } + + override func tearDown() { + sut = nil + mockRequester = nil + mockLogger = nil + mockNotificationCenter = nil + super.tearDown() + } + + func testWatchSubscriptions() { + let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called") + + mockRequester.onWatchSubscriptions = { + expectation.fulfill() + } + + sut.watchSubscriptions() + + wait(for: [expectation], timeout: 0.5) + } + + + func testWatchAppLifecycleReactsToEnterForegroundNotification() { + let setupExpectation = XCTestExpectation(description: "Expect setupTimer to be called on app enter foreground") + let watchSubscriptionsExpectation = XCTestExpectation(description: "Expect watchSubscriptions to be called on app enter foreground") + + sut.onSetupTimer = { + setupExpectation.fulfill() + } + + mockRequester.onWatchSubscriptions = { + watchSubscriptionsExpectation.fulfill() + } + + mockNotificationCenter.post(name: UIApplication.willEnterForegroundNotification) + + wait(for: [setupExpectation, watchSubscriptionsExpectation], timeout: 0.5) + } + + func testTimerTriggeringWatchSubscriptionsMultipleTimes() { + sut.timerInterval = 0.0001 + sut.setupTimer() + + let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called multiple times") + expectation.expectedFulfillmentCount = 3 + + mockRequester.onWatchSubscriptions = { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0.5) + } +}