diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift
index 8630806f5..f784fac19 100644
--- a/Example/DApp/SceneDelegate.swift
+++ b/Example/DApp/SceneDelegate.swift
@@ -24,9 +24,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
WalletConnectModal.configure(
projectId: InputConfig.projectId,
- metadata: metadata
+ metadata: metadata,
+ accentColor: .green
)
-
+
setupWindow(scene: scene)
}
diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj
index d0585642d..92c706fdd 100644
--- a/Example/ExampleApp.xcodeproj/project.pbxproj
+++ b/Example/ExampleApp.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 52;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme
index 9bdb81b02..6b4a6df22 100644
--- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme
+++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme
@@ -83,6 +83,9 @@
+
+
{
return Push.wallet.requestPublisher
}
+
+ var sessionProposalPublisher: AnyPublisher<(proposal: Session.Proposal, context: VerifyContext?), Never> {
+ return Web3Wallet.instance.sessionProposalPublisher
+ }
+
+ var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> {
+ return Web3Wallet.instance.sessionRequestPublisher
+ }
+
+ var requestPublisher: AnyPublisher<(request: AuthRequest, context: VerifyContext?), Never> {
+ return Web3Wallet.instance.authRequestPublisher
+ }
}
diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift
index dc8a74069..9c51f9869 100644
--- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift
+++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift
@@ -35,7 +35,6 @@ final class MainPresenter {
// MARK: - Private functions
extension MainPresenter {
-
private func setupInitialState() {
configurationService.configure(importAccount: importAccount)
pushRegisterer.registerForPushNotifications()
@@ -45,5 +44,25 @@ extension MainPresenter {
.sink { [unowned self] request in
router.present(pushRequest: request)
}.store(in: &disposeBag)
+
+ interactor.sessionProposalPublisher
+ .receive(on: DispatchQueue.main)
+ .sink { [unowned self] session in
+ router.present(proposal: session.proposal, importAccount: importAccount, context: session.context)
+ }
+ .store(in: &disposeBag)
+
+ interactor.sessionRequestPublisher
+ .receive(on: DispatchQueue.main)
+ .sink { [unowned self] request, context in
+ router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context)
+ }.store(in: &disposeBag)
+
+ interactor.requestPublisher
+ .receive(on: DispatchQueue.main)
+ .sink { [unowned self] result in
+ router.present(request: result.request, importAccount: importAccount, context: result.context)
+ }
+ .store(in: &disposeBag)
}
}
diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift
index e78a40f6d..11f5e2a6f 100644
--- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift
+++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift
@@ -36,4 +36,19 @@ final class MainRouter {
// PushRequestModule.create(app: app, pushRequest: pushRequest)
// .presentFullScreen(from: viewController, transparentBackground: true)
}
+
+ func present(proposal: Session.Proposal, importAccount: ImportAccount, context: VerifyContext?) {
+ SessionProposalModule.create(app: app, importAccount: importAccount, proposal: proposal, context: context)
+ .presentFullScreen(from: viewController, transparentBackground: true)
+ }
+
+ func present(sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) {
+ SessionRequestModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, sessionContext: sessionContext)
+ .presentFullScreen(from: viewController, transparentBackground: true)
+ }
+
+ func present(request: AuthRequest, importAccount: ImportAccount, context: VerifyContext?) {
+ AuthRequestModule.create(app: app, request: request, importAccount: importAccount, context: context)
+ .presentFullScreen(from: viewController, transparentBackground: true)
+ }
}
diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift
index bb806887b..1860ad808 100644
--- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift
+++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift
@@ -4,18 +4,6 @@ import Web3Wallet
import WalletConnectPush
final class WalletInteractor {
- var sessionProposalPublisher: AnyPublisher<(proposal: Session.Proposal, context: VerifyContext?), Never> {
- return Web3Wallet.instance.sessionProposalPublisher
- }
-
- var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> {
- return Web3Wallet.instance.sessionRequestPublisher
- }
-
- var requestPublisher: AnyPublisher<(request: AuthRequest, context: VerifyContext?), Never> {
- return Web3Wallet.instance.authRequestPublisher
- }
-
var sessionsPublisher: AnyPublisher<[Session], Never> {
return Web3Wallet.instance.sessionsPublisher
}
diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift
index ffd4d3a40..917d1dc94 100644
--- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift
+++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift
@@ -98,29 +98,6 @@ final class WalletPresenter: ObservableObject {
// MARK: - Private functions
extension WalletPresenter {
private func setupInitialState() {
- interactor.sessionProposalPublisher
- .receive(on: DispatchQueue.main)
- .sink { [unowned self] session in
- showPairingLoading = false
- router.present(proposal: session.proposal, importAccount: importAccount, context: session.context)
- }
- .store(in: &disposeBag)
-
- interactor.sessionRequestPublisher
- .receive(on: DispatchQueue.main)
- .sink { [unowned self] request, context in
- showPairingLoading = false
- router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context)
- }.store(in: &disposeBag)
-
- interactor.requestPublisher
- .receive(on: DispatchQueue.main)
- .sink { [unowned self] result in
- showPairingLoading = false
- router.present(request: result.request, importAccount: importAccount, context: result.context)
- }
- .store(in: &disposeBag)
-
interactor.sessionsPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] sessions in
@@ -158,10 +135,6 @@ extension WalletPresenter {
private func removePairingIndicator() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- if self.showPairingLoading {
- self.errorMessage = "WalletConnect - Pairing timeout error"
- self.showError.toggle()
- }
self.showPairingLoading = false
}
}
diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift
index e47308175..c9907a0b5 100644
--- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift
+++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift
@@ -11,20 +11,10 @@ final class WalletRouter {
self.app = app
}
- func present(proposal: Session.Proposal, importAccount: ImportAccount, context: VerifyContext?) {
- SessionProposalModule.create(app: app, importAccount: importAccount, proposal: proposal, context: context)
- .presentFullScreen(from: viewController, transparentBackground: true)
- }
-
func present(sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) {
SessionRequestModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, sessionContext: sessionContext)
.presentFullScreen(from: viewController, transparentBackground: true)
}
-
- func present(request: AuthRequest, importAccount: ImportAccount, context: VerifyContext?) {
- AuthRequestModule.create(app: app, request: request, importAccount: importAccount, context: context)
- .presentFullScreen(from: viewController, transparentBackground: true)
- }
func present(sessionProposal: Session.Proposal, importAccount: ImportAccount, sessionContext: VerifyContext?) {
SessionProposalModule.create(app: app, importAccount: importAccount, proposal: sessionProposal, context: sessionContext)
diff --git a/Sources/HTTPClient/HTTPClient.swift b/Sources/HTTPClient/HTTPClient.swift
index 0fb4c8a68..99eeca29a 100644
--- a/Sources/HTTPClient/HTTPClient.swift
+++ b/Sources/HTTPClient/HTTPClient.swift
@@ -3,4 +3,5 @@ import Foundation
public protocol HTTPClient {
func request(_ type: T.Type, at service: HTTPService) async throws -> T
func request(service: HTTPService) async throws
+ func updateHost(host: String) async
}
diff --git a/Sources/HTTPClient/HTTPError.swift b/Sources/HTTPClient/HTTPError.swift
index 446c8cbf2..959663bcc 100644
--- a/Sources/HTTPClient/HTTPError.swift
+++ b/Sources/HTTPClient/HTTPError.swift
@@ -1,10 +1,27 @@
import Foundation
-enum HTTPError: Error {
+public enum HTTPError: Error, Equatable {
case malformedURL(HTTPService)
+ case couldNotConnect
case dataTaskError(Error)
case noResponse
case badStatusCode(Int)
case responseDataNil
case jsonDecodeFailed(Error, Data)
+
+ public static func ==(lhs: HTTPError, rhs: HTTPError) -> Bool {
+ switch (lhs, rhs) {
+ case (.malformedURL, .malformedURL),
+ (.couldNotConnect, .couldNotConnect),
+ (.noResponse, .noResponse),
+ (.responseDataNil, .responseDataNil),
+ (.dataTaskError, .dataTaskError),
+ (.badStatusCode, .badStatusCode),
+ (.jsonDecodeFailed, .jsonDecodeFailed):
+ return true
+
+ default:
+ return false
+ }
+ }
}
diff --git a/Sources/HTTPClient/HTTPNetworkClient.swift b/Sources/HTTPClient/HTTPNetworkClient.swift
index 1cb5da106..a00ca2119 100644
--- a/Sources/HTTPClient/HTTPNetworkClient.swift
+++ b/Sources/HTTPClient/HTTPNetworkClient.swift
@@ -2,7 +2,7 @@ import Foundation
public actor HTTPNetworkClient: HTTPClient {
- let host: String
+ private var host: String
private let session: URLSession
@@ -31,6 +31,10 @@ public actor HTTPNetworkClient: HTTPClient {
}
}
}
+
+ public func updateHost(host: String) async {
+ self.host = host
+ }
private func request(_ type: T.Type, at service: HTTPService, completion: @escaping (Result) -> Void) {
guard let request = service.resolve(for: host) else {
@@ -67,6 +71,9 @@ public actor HTTPNetworkClient: HTTPClient {
}
private static func validate(_ urlResponse: URLResponse?, _ error: Error?) throws {
+ if let error = (error as? NSError), error.code == -1004 {
+ throw HTTPError.couldNotConnect
+ }
if let error = error {
throw HTTPError.dataTaskError(error)
}
diff --git a/Sources/WalletConnectEcho/EchoClientFactory.swift b/Sources/WalletConnectEcho/EchoClientFactory.swift
index 666498f15..3e13db72f 100644
--- a/Sources/WalletConnectEcho/EchoClientFactory.swift
+++ b/Sources/WalletConnectEcho/EchoClientFactory.swift
@@ -14,14 +14,19 @@ public struct EchoClientFactory {
environment: environment)
}
- public static func create(projectId: String,
- echoHost: String,
- keychainStorage: KeychainStorageProtocol,
- environment: APNSEnvironment) -> EchoClient {
-
+ public static func create(
+ projectId: String,
+ echoHost: String,
+ keychainStorage: KeychainStorageProtocol,
+ environment: APNSEnvironment
+ ) -> EchoClient {
+ let sessionConfiguration = URLSessionConfiguration.default
+ sessionConfiguration.timeoutIntervalForRequest = 5.0
+ sessionConfiguration.timeoutIntervalForResource = 5.0
+ let session = URLSession(configuration: sessionConfiguration)
+
let logger = ConsoleLogger(suffix: "👂🏻", loggingLevel: .debug)
-
- let httpClient = HTTPNetworkClient(host: echoHost)
+ let httpClient = HTTPNetworkClient(host: echoHost, session: session)
let clientIdStorage = ClientIdStorage(keychain: keychainStorage)
diff --git a/Sources/WalletConnectEcho/Register/EchoRegisterService.swift b/Sources/WalletConnectEcho/Register/EchoRegisterService.swift
index b3df306ce..702ba10cb 100644
--- a/Sources/WalletConnectEcho/Register/EchoRegisterService.swift
+++ b/Sources/WalletConnectEcho/Register/EchoRegisterService.swift
@@ -7,7 +7,10 @@ actor EchoRegisterService {
private let environment: APNSEnvironment
private let echoAuthenticator: EchoAuthenticating
private let clientIdStorage: ClientIdStoring
-
+ /// The property is used to determine whether echo.walletconnect.org will be used
+ /// in case echo.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location).
+ private var fallback = false
+
enum Errors: Error {
case registrationFailed
}
@@ -33,14 +36,28 @@ actor EchoRegisterService {
let clientId = try clientIdStorage.getClientId()
let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519)
logger.debug("APNS device token: \(token)")
- let response = try await httpClient.request(
- EchoResponse.self,
- at: EchoAPI.register(clientId: clientIdMutlibase, token: token, projectId: projectId, environment: environment, auth: echoAuthToken)
- )
- guard response.status == .success else {
- throw Errors.registrationFailed
+
+ do {
+ let response = try await httpClient.request(
+ EchoResponse.self,
+ at: EchoAPI.register(clientId: clientIdMutlibase, token: token, projectId: projectId, environment: environment, auth: echoAuthToken)
+ )
+ guard response.status == .success else {
+ throw Errors.registrationFailed
+ }
+ logger.debug("Successfully registered at Echo Server")
+ } catch {
+ if (error as? HTTPError) == .couldNotConnect && !fallback {
+ fallback = true
+ await echoHostFallback()
+ try await register(deviceToken: deviceToken)
+ }
+ throw error
}
- logger.debug("Successfully registered at Echo Server")
+ }
+
+ func echoHostFallback() async {
+ await httpClient.updateHost(host: "echo.walletconnect.org")
}
#if DEBUG
@@ -48,14 +65,24 @@ actor EchoRegisterService {
let echoAuthToken = try echoAuthenticator.createAuthToken()
let clientId = try clientIdStorage.getClientId()
let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519)
- let response = try await httpClient.request(
- EchoResponse.self,
- at: EchoAPI.register(clientId: clientIdMutlibase, token: deviceToken, projectId: projectId, environment: environment, auth: echoAuthToken)
- )
- guard response.status == .success else {
- throw Errors.registrationFailed
+
+ do {
+ let response = try await httpClient.request(
+ EchoResponse.self,
+ at: EchoAPI.register(clientId: clientIdMutlibase, token: deviceToken, projectId: projectId, environment: environment, auth: echoAuthToken)
+ )
+ guard response.status == .success else {
+ throw Errors.registrationFailed
+ }
+ logger.debug("Successfully registered at Echo Server")
+ } catch {
+ if (error as? HTTPError) == .couldNotConnect && !fallback {
+ fallback = true
+ await echoHostFallback()
+ try await register(deviceToken: deviceToken)
+ }
+ throw error
}
- logger.debug("Successfully registered at Echo Server")
}
#endif
}
diff --git a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift
index 7206886ea..0c8af0885 100644
--- a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift
+++ b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift
@@ -4,9 +4,63 @@ import Foundation
extension Listing {
static let stubList: [Listing] = [
- Listing(id: UUID().uuidString, name: "Sample Wallet", homepage: "https://example.com", order: 1, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "sampleapp://deeplink", universal: "https://example.com/universal")),
- Listing(id: UUID().uuidString, name: "Awesome Wallet", homepage: "https://example.com/awesome", order: 2, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "awesomeapp://deeplink", universal: "https://example.com/awesome/universal")),
- Listing(id: UUID().uuidString, name: "Cool Wallet", homepage: "https://example.com/cool", order: 3, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "coolapp://deeplink", universal: "https://example.com/cool/universal"))
+ Listing(
+ id: UUID().uuidString,
+ name: "Sample Wallet",
+ homepage: "https://example.com",
+ order: 1,
+ imageId: UUID().uuidString,
+ app: Listing.App(
+ ios: "https://example.com/download-ios",
+ browser: "https://example.com/download-safari"
+ ),
+ mobile: .init(
+ native: "sampleapp://deeplink",
+ universal: "https://example.com/universal"
+ ),
+ desktop: .init(
+ native: nil,
+ universal: "https://example.com/universal"
+ )
+ ),
+ Listing(
+ id: UUID().uuidString,
+ name: "Awesome Wallet",
+ homepage: "https://example.com/awesome",
+ order: 2,
+ imageId: UUID().uuidString,
+ app: Listing.App(
+ ios: "https://example.com/download-ios",
+ browser: "https://example.com/download-safari"
+ ),
+ mobile: .init(
+ native: "awesomeapp://deeplink",
+ universal: "https://example.com/awesome/universal"
+ ),
+ desktop: .init(
+ native: nil,
+ universal: "https://example.com/awesome/universal"
+ )
+ ),
+ Listing(
+ id: UUID().uuidString,
+ name: "Cool Wallet",
+ homepage: "https://example.com/cool",
+ order: 3,
+ imageId: UUID().uuidString,
+ app: Listing.App(
+ ios: "https://example.com/download-ios",
+ browser: "https://example.com/download-safari"
+ ),
+ mobile: .init(
+ native: "coolapp://deeplink",
+ universal: "https://example.com/cool/universal"
+ ),
+ desktop: .init(
+ native: nil,
+ universal: "https://example.com/cool/universal"
+ )
+ )
]
}
diff --git a/Sources/WalletConnectModal/Modal/ModalSheet.swift b/Sources/WalletConnectModal/Modal/ModalSheet.swift
index 64c9bb78d..c51e69d2b 100644
--- a/Sources/WalletConnectModal/Modal/ModalSheet.swift
+++ b/Sources/WalletConnectModal/Modal/ModalSheet.swift
@@ -14,7 +14,6 @@ public struct ModalSheet: View {
VStack(spacing: 0) {
contentHeader()
content()
-
}
.frame(maxWidth: .infinity)
.background(Color.background1)
@@ -75,13 +74,12 @@ public struct ModalSheet: View {
EmptyView()
}
}
- .animation(.default)
+ .animation(.default, value: viewModel.destination)
.foregroundColor(.accent)
.frame(height: 60)
.overlay(
VStack {
if viewModel.destination.hasSearch {
-
HStack {
Image(systemName: "magnifyingglass")
TextField("Search", text: $viewModel.searchTerm, onEditingChanged: { editing in
@@ -128,7 +126,7 @@ public struct ModalSheet: View {
viewModel.destination
}, set: { _ in }),
navigateTo: viewModel.navigateTo(_:),
- onListingTap: { viewModel.onListingTap($0, preferUniversal: false) }
+ onListingTap: { viewModel.onListingTap($0) }
)
}
@@ -144,7 +142,6 @@ public struct ModalSheet: View {
@ViewBuilder
private func content() -> some View {
-
switch viewModel.destination {
case .welcome,
.viewAll:
@@ -155,7 +152,7 @@ public struct ModalSheet: View {
case .getWallet:
GetAWalletView(
wallets: Array(viewModel.wallets.prefix(6)),
- onWalletTap: viewModel.onGetWalletTap(_:),
+ onWalletTap: viewModel.openAppstore(wallet:),
navigateToExternalLink: viewModel.navigateToExternalLink(_:)
)
.frame(minHeight: verticalSizeClass == .compact ? 200 : 550)
@@ -163,10 +160,10 @@ public struct ModalSheet: View {
case let .walletDetail(wallet):
WalletDetail(
- wallet: wallet,
- deeplink: { viewModel.onListingTap($0, preferUniversal: false) },
- deeplinkUniversal: { viewModel.onListingTap($0, preferUniversal: true) },
- openAppStore: viewModel.onGetWalletTap(_:)
+ viewModel: .init(
+ wallet: wallet,
+ deeplinkHandler: viewModel
+ )
)
}
}
diff --git a/Sources/WalletConnectModal/Modal/ModalViewModel.swift b/Sources/WalletConnectModal/Modal/ModalViewModel.swift
index bdd5e9a9f..33b26c141 100644
--- a/Sources/WalletConnectModal/Modal/ModalViewModel.swift
+++ b/Sources/WalletConnectModal/Modal/ModalViewModel.swift
@@ -118,23 +118,8 @@ final class ModalViewModel: ObservableObject {
uiApplicationWrapper.openURL(url, nil)
}
- func onListingTap(_ listing: Listing, preferUniversal: Bool) {
+ func onListingTap(_ listing: Listing) {
setLastTimeUsed(listing.id)
-
- navigateToDeepLink(
- universalLink: listing.mobile.universal ?? "",
- nativeLink: listing.mobile.native ?? "",
- preferUniversal: preferUniversal
- )
- }
-
- func onGetWalletTap(_ listing: Listing) {
- guard
- let storeLinkString = listing.app.ios,
- let storeLink = URL(string: storeLinkString)
- else { return }
-
- uiApplicationWrapper.openURL(storeLink, nil)
}
func onBackButton() {
@@ -257,28 +242,29 @@ private extension ModalViewModel {
// MARK: - Deeplinking
-private extension ModalViewModel {
- enum DeeplinkErrors: LocalizedError {
- case noWalletLinkFound
- case uriNotCreated
- case failedToOpen
+protocol WalletDeeplinkHandler {
+ func openAppstore(wallet: Listing)
+ func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool)
+}
+
+extension ModalViewModel: WalletDeeplinkHandler {
+
+ func openAppstore(wallet: Listing) {
+ guard
+ let storeLinkString = wallet.app.ios,
+ let storeLink = URL(string: storeLinkString)
+ else { return }
- var errorDescription: String? {
- switch self {
- case .noWalletLinkFound:
- return NSLocalizedString("No valid link for opening given wallet found", comment: "")
- case .uriNotCreated:
- return NSLocalizedString("Couldn't generate link due to missing connection URI", comment: "")
- case .failedToOpen:
- return NSLocalizedString("Given link couldn't be opened", comment: "")
- }
- }
+ uiApplicationWrapper.openURL(storeLink, nil)
}
-
- func navigateToDeepLink(universalLink: String, nativeLink: String, preferUniversal: Bool) {
+
+ func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool) {
do {
- let nativeUrlString = try formatNativeUrlString(nativeLink)
- let universalUrlString = try formatUniversalUrlString(universalLink)
+ let nativeScheme = preferBrowser ? nil : wallet.mobile.native
+ let universalScheme = preferBrowser ? wallet.desktop.universal : wallet.mobile.universal
+
+ let nativeUrlString = try formatNativeUrlString(nativeScheme)
+ let universalUrlString = try formatUniversalUrlString(universalScheme)
if let nativeUrl = nativeUrlString?.toURL(), !preferUniversal {
uiApplicationWrapper.openURL(nativeUrl) { success in
@@ -299,13 +285,32 @@ private extension ModalViewModel {
toast = Toast(style: .error, message: error.localizedDescription)
}
}
+}
+
+private extension ModalViewModel {
+ enum DeeplinkErrors: LocalizedError {
+ case noWalletLinkFound
+ case uriNotCreated
+ case failedToOpen
+
+ var errorDescription: String? {
+ switch self {
+ case .noWalletLinkFound:
+ return NSLocalizedString("No valid link for opening given wallet found", comment: "")
+ case .uriNotCreated:
+ return NSLocalizedString("Couldn't generate link due to missing connection URI", comment: "")
+ case .failedToOpen:
+ return NSLocalizedString("Given link couldn't be opened", comment: "")
+ }
+ }
+ }
func isHttpUrl(url: String) -> Bool {
return url.hasPrefix("http://") || url.hasPrefix("https://")
}
- func formatNativeUrlString(_ string: String) throws -> String? {
- if string.isEmpty { return nil }
+ func formatNativeUrlString(_ string: String?) throws -> String? {
+ guard let string = string, !string.isEmpty else { return nil }
if isHttpUrl(url: string) {
return try formatUniversalUrlString(string)
@@ -324,8 +329,8 @@ private extension ModalViewModel {
return "\(safeAppUrl)wc?uri=\(deeplinkUri)"
}
- func formatUniversalUrlString(_ string: String) throws -> String? {
- if string.isEmpty { return nil }
+ func formatUniversalUrlString(_ string: String?) throws -> String? {
+ guard let string = string, !string.isEmpty else { return nil }
if !isHttpUrl(url: string) {
return try formatNativeUrlString(string)
diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift
similarity index 50%
rename from Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift
rename to Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift
index c8f56798c..3bae82d17 100644
--- a/Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift
+++ b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift
@@ -3,29 +3,71 @@ import SwiftUI
struct WalletDetail: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
- @State var wallet: Listing
- @State var retryShown: Bool = false
+ @ObservedObject var viewModel: WalletDetailViewModel
- let deeplink: (Listing) -> Void
- var deeplinkUniversal: (Listing) -> Void
- var openAppStore: (Listing) -> Void
+ @State var retryShown: Bool = false
var body: some View {
- content()
- .onAppear {
- if verticalSizeClass == .compact {
- retryShown = true
- } else {
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
- withAnimation {
- retryShown = true
+ VStack {
+ if viewModel.showToggle {
+ Web3ModalPicker(
+ WalletDetailViewModel.Platform.allCases,
+ selection: viewModel.preferredPlatform
+ ) { item in
+
+ HStack {
+ switch item {
+ case .native:
+ Image(systemName: "iphone")
+ case .browser:
+ Image(systemName: "safari")
+ }
+ Text(item.rawValue.capitalized)
+ }
+ .font(.system(size: 14).weight(.semibold))
+ .multilineTextAlignment(.center)
+ .foregroundColor(viewModel.preferredPlatform == item ? .foreground1 : .foreground2)
+ .frame(maxWidth: .infinity)
+ .contentShape(Rectangle())
+ .padding(.horizontal, 8)
+ .padding(.vertical, 8)
+ .onTapGesture {
+ withAnimation(.easeInOut(duration: 0.15)) {
+ viewModel.preferredPlatform = item
}
}
}
+ .pickerBackgroundColor(.background2)
+ .cornerRadius(20)
+ .borderWidth(1)
+ .borderColor(.thinOverlay)
+ .accentColor(.thinOverlay)
+ .frame(maxWidth: 250)
+ .padding()
}
- .onDisappear {
- retryShown = false
- }
+
+ content()
+ .onAppear {
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ viewModel.handle(.onAppear)
+ }
+
+ if verticalSizeClass == .compact {
+ retryShown = true
+ } else {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
+ withAnimation {
+ retryShown = true
+ }
+ }
+ }
+ }
+ .onDisappear {
+ retryShown = false
+ }
+ .animation(.easeInOut, value: viewModel.preferredPlatform)
+ }
}
@ViewBuilder
@@ -41,9 +83,11 @@ struct WalletDetail: View {
retrySection()
}
- Divider()
-
- appStoreRow()
+ VStack {
+ Divider()
+ appStoreRow()
+ }
+ .opacity(viewModel.preferredPlatform != .native ? 0 : 1)
}
.padding(.horizontal, 20)
}
@@ -56,12 +100,15 @@ struct WalletDetail: View {
VStack(spacing: 15) {
if retryShown {
retrySection()
+ .frame(maxWidth: .infinity)
.padding(.top, 15)
}
- Divider()
-
- appStoreRow()
+ VStack {
+ Divider()
+ appStoreRow()
+ }
+ .opacity(viewModel.preferredPlatform != .native ? 0 : 1)
}
.padding(.horizontal, 20)
.padding(.bottom, 40)
@@ -72,7 +119,7 @@ struct WalletDetail: View {
func walletImage() -> some View {
VStack(spacing: 20) {
- WalletImage(wallet: wallet, size: .large)
+ WalletImage(wallet: viewModel.wallet, size: .large)
.frame(width: 96, height: 96)
.cornerRadius(24)
.overlay(
@@ -80,7 +127,7 @@ struct WalletDetail: View {
.stroke(.gray.opacity(0.4), lineWidth: 1)
)
- Text("Continue in \(wallet.name)...")
+ Text("Continue in \(viewModel.wallet.name)...")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.foreground1)
}
@@ -88,10 +135,7 @@ struct WalletDetail: View {
func retrySection() -> some View {
VStack(spacing: 15) {
- let hasUniversalLink = wallet.mobile.universal?.isEmpty == false
- let hasNativeLink = wallet.mobile.native?.isEmpty == false
-
- Text("You can try opening \(wallet.name) again \((hasNativeLink && hasUniversalLink) ? "or try using a Universal Link instead" : "")")
+ Text("You can try opening \(viewModel.wallet.name) again \((viewModel.hasNativeLink && viewModel.showUniversalLink) ? "or try using a Universal Link instead" : "")")
.font(.system(size: 14, weight: .medium))
.multilineTextAlignment(.center)
.foregroundColor(.foreground2)
@@ -99,15 +143,15 @@ struct WalletDetail: View {
HStack {
Button {
- deeplink(wallet)
+ viewModel.handle(.didTapTryAgain)
} label: {
Text("Try Again")
}
.buttonStyle(WCMAccentButtonStyle())
- if hasUniversalLink {
+ if viewModel.showUniversalLink {
Button {
- deeplinkUniversal(wallet)
+ viewModel.handle(.didTapUniversalLink)
} label: {
Text("Universal link")
}
@@ -115,16 +159,17 @@ struct WalletDetail: View {
}
}
}
+ .frame(height: 100)
}
func appStoreRow() -> some View {
HStack(spacing: 0) {
HStack(spacing: 10) {
- WalletImage(wallet: wallet, size: .small)
+ WalletImage(wallet: viewModel.wallet, size: .small)
.frame(width: 28, height: 28)
.cornerRadius(8)
- Text("Get \(wallet.name)")
+ Text("Get \(viewModel.wallet.name)")
.font(.system(size: 16).weight(.semibold))
.foregroundColor(.foreground1)
}
@@ -141,7 +186,7 @@ struct WalletDetail: View {
}
}
.onTapGesture {
- openAppStore(wallet)
+ viewModel.handle(.didTapAppStore)
}
}
}
diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift
new file mode 100644
index 000000000..4b146927c
--- /dev/null
+++ b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+final class WalletDetailViewModel: ObservableObject {
+ enum Platform: String, CaseIterable, Identifiable {
+ case native
+ case browser
+
+ var id: Self { self }
+ }
+
+ enum Event {
+ case onAppear
+ case didTapUniversalLink
+ case didTapTryAgain
+ case didTapAppStore
+ }
+
+ let wallet: Listing
+ let deeplinkHandler: WalletDeeplinkHandler
+
+ @Published var preferredPlatform: Platform = .native
+
+ var showToggle: Bool { wallet.app.browser != nil && wallet.app.ios != nil }
+ var showUniversalLink: Bool { preferredPlatform == .native && wallet.mobile.universal?.isEmpty == false }
+ var hasNativeLink: Bool { wallet.mobile.native?.isEmpty == false }
+
+ init(
+ wallet: Listing,
+ deeplinkHandler: WalletDeeplinkHandler
+ ) {
+ self.wallet = wallet
+ self.deeplinkHandler = deeplinkHandler
+ preferredPlatform = wallet.app.ios != nil ? .native : .browser
+ }
+
+ func handle(_ event: Event) {
+ switch event {
+ case .onAppear:
+ deeplinkHandler.navigateToDeepLink(
+ wallet: wallet,
+ preferUniversal: true,
+ preferBrowser: preferredPlatform == .browser
+ )
+
+ case .didTapUniversalLink:
+ deeplinkHandler.navigateToDeepLink(
+ wallet: wallet,
+ preferUniversal: true,
+ preferBrowser: preferredPlatform == .browser
+ )
+
+ case .didTapTryAgain:
+ deeplinkHandler.navigateToDeepLink(
+ wallet: wallet,
+ preferUniversal: false,
+ preferBrowser: preferredPlatform == .browser
+ )
+
+ case .didTapAppStore:
+ deeplinkHandler.openAppstore(wallet: wallet)
+ }
+ }
+}
diff --git a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift b/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift
index bc7c91619..3eef522e8 100644
--- a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift
+++ b/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift
@@ -11,8 +11,9 @@ struct Listing: Codable, Hashable, Identifiable {
let order: Int?
let imageId: String
let app: App
- let mobile: Mobile
- var lastTimeUsed: Date?
+ let mobile: Links
+ let desktop: Links
+ var lastTimeUsed: Date?
private enum CodingKeys: String, CodingKey {
case id
@@ -22,16 +23,16 @@ struct Listing: Codable, Hashable, Identifiable {
case imageId = "image_id"
case app
case mobile
+ case desktop
case lastTimeUsed
}
struct App: Codable, Hashable {
let ios: String?
- let mac: String?
- let safari: String?
+ let browser: String?
}
- struct Mobile: Codable, Hashable {
+ struct Links: Codable, Hashable {
let native: String?
let universal: String?
}
diff --git a/Sources/WalletConnectModal/Resources/Color.swift b/Sources/WalletConnectModal/Resources/Color.swift
index 35fa0a860..3a01b85f8 100644
--- a/Sources/WalletConnectModal/Resources/Color.swift
+++ b/Sources/WalletConnectModal/Resources/Color.swift
@@ -30,7 +30,7 @@ extension Color {
static let negative = Color(AssetColor.negative)
static let thickOverlay = Color(AssetColor.thickOverlay)
static let thinOverlay = Color(AssetColor.thinOverlay)
- static let accent = Color(AssetColor.accent)
+ static var accent = Color(AssetColor.accent)
}
#if canImport(UIKit)
diff --git a/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift b/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift
new file mode 100644
index 000000000..0822b7fc9
--- /dev/null
+++ b/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift
@@ -0,0 +1,127 @@
+import SwiftUI
+
+struct Web3ModalPicker: View where Data: Hashable, Content: View {
+ let sources: [Data]
+ let selection: Data?
+ let itemBuilder: (Data) -> Content
+
+ @State private var backgroundColor: Color = Color.black.opacity(0.05)
+
+ func pickerBackgroundColor(_ color: Color) -> Web3ModalPicker {
+ var view = self
+ view._backgroundColor = State(initialValue: color)
+ return view
+ }
+
+ @State private var cornerRadius: CGFloat?
+
+ func cornerRadius(_ cornerRadius: CGFloat) -> Web3ModalPicker {
+ var view = self
+ view._cornerRadius = State(initialValue: cornerRadius)
+ return view
+ }
+
+ @State private var borderColor: Color?
+
+ func borderColor(_ borderColor: Color) -> Web3ModalPicker {
+ var view = self
+ view._borderColor = State(initialValue: borderColor)
+ return view
+ }
+
+ @State private var borderWidth: CGFloat?
+
+ func borderWidth(_ borderWidth: CGFloat) -> Web3ModalPicker {
+ var view = self
+ view._borderWidth = State(initialValue: borderWidth)
+ return view
+ }
+
+ private var customIndicator: AnyView?
+
+ init(
+ _ sources: [Data],
+ selection: Data?,
+ @ViewBuilder itemBuilder: @escaping (Data) -> Content
+ ) {
+ self.sources = sources
+ self.selection = selection
+ self.itemBuilder = itemBuilder
+ }
+
+ public var body: some View {
+ ZStack(alignment: .center) {
+ if let selection = selection, let selectedIdx = sources.firstIndex(of: selection) {
+
+ GeometryReader { geo in
+ RoundedRectangle(cornerRadius: cornerRadius ?? 6.0)
+ .stroke(borderColor ?? .clear, lineWidth: borderWidth ?? 0)
+ .foregroundColor(.accentColor)
+ .padding(EdgeInsets(top: borderWidth ?? 2, leading: borderWidth ?? 2, bottom: borderWidth ?? 2, trailing: borderWidth ?? 2))
+ .frame(width: geo.size.width / CGFloat(sources.count))
+ .animation(.spring().speed(1.5), value: selection)
+ .offset(x: geo.size.width / CGFloat(sources.count) * CGFloat(selectedIdx), y: 0)
+ }.frame(height: 32)
+ }
+
+ HStack(spacing: 0) {
+ ForEach(sources, id: \.self) { item in
+ itemBuilder(item)
+ }
+ }
+ }
+ .background(
+ RoundedRectangle(cornerRadius: cornerRadius ?? 6.0)
+ .fill(backgroundColor)
+ .padding(-5)
+ )
+ }
+}
+
+struct PreviewWeb3ModalPicker: View {
+
+ enum Platform: String, CaseIterable {
+ case native
+ case browser
+ }
+
+ @State private var selectedItem: Platform? = .native
+
+ var body: some View {
+ Web3ModalPicker(
+ Platform.allCases,
+ selection: selectedItem
+ ) { item in
+
+ HStack {
+ Image(systemName: "iphone")
+ Text(item.rawValue.capitalized)
+ }
+ .font(.system(size: 14).weight(.semibold))
+ .multilineTextAlignment(.center)
+ .foregroundColor(selectedItem == item ? .foreground1 : .foreground2)
+ .frame(maxWidth: .infinity)
+ .contentShape(Rectangle())
+ .padding(.horizontal, 8)
+ .padding(.vertical, 8)
+ .onTapGesture {
+ withAnimation(.easeInOut(duration: 0.15)) {
+ selectedItem = item
+ }
+ }
+ }
+ .pickerBackgroundColor(.background2)
+ .cornerRadius(20)
+ .borderWidth(1)
+ .borderColor(.thinOverlay)
+ .accentColor(.thinOverlay)
+ .frame(maxWidth: 250)
+ .padding()
+ }
+}
+
+struct Web3ModalPicker_Previews: PreviewProvider {
+ static var previews: some View {
+ PreviewWeb3ModalPicker()
+ }
+}
diff --git a/Sources/WalletConnectModal/WalletConnectModal.swift b/Sources/WalletConnectModal/WalletConnectModal.swift
index d405d9178..7fe3c3674 100644
--- a/Sources/WalletConnectModal/WalletConnectModal.swift
+++ b/Sources/WalletConnectModal/WalletConnectModal.swift
@@ -1,4 +1,5 @@
import Foundation
+import SwiftUI
#if canImport(UIKit)
import UIKit
@@ -53,7 +54,8 @@ public class WalletConnectModal {
metadata: AppMetadata,
sessionParams: SessionParams = .default,
recommendedWalletIds: [String] = [],
- excludedWalletIds: [String] = []
+ excludedWalletIds: [String] = [],
+ accentColor: Color? = nil
) {
Pair.configure(metadata: metadata)
WalletConnectModal.config = WalletConnectModal.Config(
@@ -63,6 +65,10 @@ public class WalletConnectModal {
recommendedWalletIds: recommendedWalletIds,
excludedWalletIds: excludedWalletIds
)
+
+ if let accentColor {
+ Color.accent = accentColor
+ }
}
public static func set(sessionParams: SessionParams) {
diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift
index ca15160a8..507e82603 100644
--- a/Sources/WalletConnectNetworking/NetworkInteracting.swift
+++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift
@@ -3,6 +3,7 @@ import Combine
public protocol NetworkInteracting {
var socketConnectionStatusPublisher: AnyPublisher { get }
+ var networkConnectionStatusPublisher: AnyPublisher { get }
var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { get }
func subscribe(topic: String) async throws
func unsubscribe(topic: String)
diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift
index d51379c97..76135c8fa 100644
--- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift
+++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift
@@ -1,7 +1,6 @@
import Foundation
import Combine
-
public class NetworkingInteractor: NetworkInteracting {
private var publishers = Set()
private let relayClient: RelayClient
@@ -24,8 +23,10 @@ public class NetworkingInteractor: NetworkInteracting {
logger.logsPublisher.eraseToAnyPublisher()
}
-
+ public var networkConnectionStatusPublisher: AnyPublisher
public var socketConnectionStatusPublisher: AnyPublisher
+
+ private let networkMonitor: NetworkMonitoring
public init(
relayClient: RelayClient,
@@ -38,6 +39,8 @@ public class NetworkingInteractor: NetworkInteracting {
self.rpcHistory = rpcHistory
self.logger = logger
self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher
+ self.networkMonitor = NetworkMonitor()
+ self.networkConnectionStatusPublisher = networkMonitor.networkConnectionStatusPublisher
setupRelaySubscribtion()
}
diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift
index 389c11354..3eafd2838 100644
--- a/Sources/WalletConnectPairing/PairingClient.swift
+++ b/Sources/WalletConnectPairing/PairingClient.swift
@@ -7,6 +7,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient
}
public let socketConnectionStatusPublisher: AnyPublisher
+ private let pairingStorage: WCPairingStorage
private let walletPairService: WalletPairService
private let appPairService: AppPairService
private let appPairActivateService: AppPairActivationService
@@ -22,20 +23,23 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient
private let cleanupService: PairingCleanupService
- init(appPairService: AppPairService,
- networkingInteractor: NetworkInteracting,
- logger: ConsoleLogging,
- walletPairService: WalletPairService,
- deletePairingService: DeletePairingService,
- resubscribeService: PairingResubscribeService,
- expirationService: ExpirationService,
- pairingRequestsSubscriber: PairingRequestsSubscriber,
- appPairActivateService: AppPairActivationService,
- cleanupService: PairingCleanupService,
- pingService: PairingPingService,
- socketConnectionStatusPublisher: AnyPublisher,
- pairingsProvider: PairingsProvider
+ init(
+ pairingStorage: WCPairingStorage,
+ appPairService: AppPairService,
+ networkingInteractor: NetworkInteracting,
+ logger: ConsoleLogging,
+ walletPairService: WalletPairService,
+ deletePairingService: DeletePairingService,
+ resubscribeService: PairingResubscribeService,
+ expirationService: ExpirationService,
+ pairingRequestsSubscriber: PairingRequestsSubscriber,
+ appPairActivateService: AppPairActivationService,
+ cleanupService: PairingCleanupService,
+ pingService: PairingPingService,
+ socketConnectionStatusPublisher: AnyPublisher,
+ pairingsProvider: PairingsProvider
) {
+ self.pairingStorage = pairingStorage
self.appPairService = appPairService
self.walletPairService = walletPairService
self.networkingInteractor = networkingInteractor
@@ -81,6 +85,15 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient
public func activate(pairingTopic: String, peerMetadata: AppMetadata?) {
appPairActivateService.activate(for: pairingTopic, peerMetadata: peerMetadata)
}
+
+ public func setReceived(pairingTopic: String) {
+ guard var pairing = pairingStorage.getPairing(forTopic: pairingTopic) else {
+ return logger.error("Pairing not found for topic: \(pairingTopic)")
+ }
+
+ pairing.receivedRequest()
+ pairingStorage.setPairing(pairing)
+ }
public func getPairings() -> [Pairing] {
pairingsProvider.getPairings()
diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift
index 8666747ac..9706702af 100644
--- a/Sources/WalletConnectPairing/PairingClientFactory.swift
+++ b/Sources/WalletConnectPairing/PairingClientFactory.swift
@@ -24,6 +24,7 @@ public struct PairingClientFactory {
let resubscribeService = PairingResubscribeService(networkInteractor: networkingClient, pairingStorage: pairingStore)
return PairingClient(
+ pairingStorage: pairingStore,
appPairService: appPairService,
networkingInteractor: networkingClient,
logger: logger,
diff --git a/Sources/WalletConnectPairing/PairingRegisterer.swift b/Sources/WalletConnectPairing/PairingRegisterer.swift
index 6aa017038..e09cf9a42 100644
--- a/Sources/WalletConnectPairing/PairingRegisterer.swift
+++ b/Sources/WalletConnectPairing/PairingRegisterer.swift
@@ -7,5 +7,6 @@ public protocol PairingRegisterer {
) -> AnyPublisher, Never>
func activate(pairingTopic: String, peerMetadata: AppMetadata?)
+ func setReceived(pairingTopic: String)
func validatePairingExistance(_ topic: String) throws
}
diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
index cfbd6c930..d14d82e3c 100644
--- a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
+++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
@@ -3,6 +3,7 @@ import Foundation
actor WalletPairService {
enum Errors: Error {
case pairingAlreadyExist(topic: String)
+ case networkNotConnected
}
let networkingInteractor: NetworkInteracting
@@ -21,16 +22,42 @@ actor WalletPairService {
guard !hasPairing(for: uri.topic) else {
throw Errors.pairingAlreadyExist(topic: uri.topic)
}
- var pairing = WCPairing(uri: uri)
+
+ let pairing = WCPairing(uri: uri)
let symKey = try SymmetricKey(hex: uri.symKey)
try kms.setSymmetricKey(symKey, for: pairing.topic)
- pairing.activate()
pairingStorage.setPairing(pairing)
+
+ let networkConnectionStatus = await resolveNetworkConnectionStatus()
+ guard networkConnectionStatus == .connected else {
+ throw Errors.networkNotConnected
+ }
+
try await networkingInteractor.subscribe(topic: pairing.topic)
}
+}
+// MARK: - Private functions
+extension WalletPairService {
func hasPairing(for topic: String) -> Bool {
- return pairingStorage.hasPairing(forTopic: topic)
+ if let pairing = pairingStorage.getPairing(forTopic: topic) {
+ return pairing.requestReceived
+ }
+ return false
+ }
+
+ private func resolveNetworkConnectionStatus() async -> NetworkConnectionStatus {
+ return await withCheckedContinuation { continuation in
+ let cancellable = networkingInteractor.networkConnectionStatusPublisher.sink { value in
+ continuation.resume(returning: value)
+ }
+
+ Task(priority: .high) {
+ await withTaskCancellationHandler {
+ cancellable.cancel()
+ } onCancel: { }
+ }
+ }
}
}
@@ -38,7 +65,8 @@ actor WalletPairService {
extension WalletPairService.Errors: LocalizedError {
var errorDescription: String? {
switch self {
- case .pairingAlreadyExist(let topic): return "Pairing with topic (\(topic)) already exist"
+ case .pairingAlreadyExist(let topic): return "Pairing with topic (\(topic)) already exists. Use 'Web3Wallet.instance.getPendingProposals()' or 'Web3Wallet.instance.getPendingRequests()' in order to receive proposals or requests."
+ case .networkNotConnected: return "Pairing failed. You seem to be offline"
}
}
}
diff --git a/Sources/WalletConnectPairing/Types/WCPairing.swift b/Sources/WalletConnectPairing/Types/WCPairing.swift
index 6ceb323cd..d87bd8946 100644
--- a/Sources/WalletConnectPairing/Types/WCPairing.swift
+++ b/Sources/WalletConnectPairing/Types/WCPairing.swift
@@ -11,6 +11,7 @@ public struct WCPairing: SequenceObject {
public private (set) var peerMetadata: AppMetadata?
public private (set) var expiryDate: Date
public private (set) var active: Bool
+ public private (set) var requestReceived: Bool
#if DEBUG
public static var dateInitializer: () -> Date = Date.init
@@ -26,11 +27,12 @@ public struct WCPairing: SequenceObject {
30 * .day
}
- public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, expiryDate: Date) {
+ public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, requestReceived: Bool = false, expiryDate: Date) {
self.topic = topic
self.relay = relay
self.peerMetadata = peerMetadata
self.active = isActive
+ self.requestReceived = requestReceived
self.expiryDate = expiryDate
}
@@ -38,6 +40,7 @@ public struct WCPairing: SequenceObject {
self.topic = topic
self.relay = RelayProtocolOptions(protocol: "irn", data: nil)
self.active = false
+ self.requestReceived = false
self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive)
}
@@ -45,6 +48,7 @@ public struct WCPairing: SequenceObject {
self.topic = uri.topic
self.relay = uri.relay
self.active = false
+ self.requestReceived = false
self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive)
}
@@ -52,6 +56,10 @@ public struct WCPairing: SequenceObject {
active = true
try? updateExpiry()
}
+
+ public mutating func receivedRequest() {
+ requestReceived = true
+ }
public mutating func updatePeerMetadata(_ metadata: AppMetadata?) {
peerMetadata = metadata
diff --git a/Sources/WalletConnectRelay/NetworkMonitoring.swift b/Sources/WalletConnectRelay/NetworkMonitoring.swift
index c4200171f..1d3932db5 100644
--- a/Sources/WalletConnectRelay/NetworkMonitoring.swift
+++ b/Sources/WalletConnectRelay/NetworkMonitoring.swift
@@ -1,27 +1,32 @@
import Foundation
+import Combine
import Network
-protocol NetworkMonitoring: AnyObject {
- var onSatisfied: (() -> Void)? {get set}
- var onUnsatisfied: (() -> Void)? {get set}
- func startMonitoring()
+public enum NetworkConnectionStatus {
+ case connected
+ case notConnected
}
-class NetworkMonitor: NetworkMonitoring {
- var onSatisfied: (() -> Void)?
- var onUnsatisfied: (() -> Void)?
-
- private let monitor = NWPathMonitor()
- private let monitorQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor")
+public protocol NetworkMonitoring: AnyObject {
+ var networkConnectionStatusPublisher: AnyPublisher { get }
+}
- func startMonitoring() {
- monitor.pathUpdateHandler = { [weak self] path in
- if path.status == .satisfied {
- self?.onSatisfied?()
- } else {
- self?.onUnsatisfied?()
- }
+public final class NetworkMonitor: NetworkMonitoring {
+ private let networkMonitor = NWPathMonitor()
+ private let workerQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor")
+
+ private let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected)
+
+ public var networkConnectionStatusPublisher: AnyPublisher {
+ networkConnectionStatusPublisherSubject
+ .share()
+ .eraseToAnyPublisher()
+ }
+
+ public init() {
+ networkMonitor.pathUpdateHandler = { [weak self] path in
+ self?.networkConnectionStatusPublisherSubject.send((path.status == .satisfied) ? .connected : .notConnected)
}
- monitor.start(queue: monitorQueue)
+ networkMonitor.start(queue: workerQueue)
}
}
diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json
index c331e32ba..9ed5ec2e3 100644
--- a/Sources/WalletConnectRelay/PackageConfig.json
+++ b/Sources/WalletConnectRelay/PackageConfig.json
@@ -1 +1 @@
-{"version": "1.6.17"}
+{"version": "1.6.18"}
diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
index 804ee14f0..99c61c61c 100644
--- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
+++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
@@ -45,10 +45,12 @@ class AutomaticSocketConnectionHandler {
}
private func setUpNetworkMonitoring() {
- networkMonitor.onSatisfied = { [weak self] in
- self?.reconnectIfNeeded()
+ networkMonitor.networkConnectionStatusPublisher.sink { [weak self] networkConnectionStatus in
+ if networkConnectionStatus == .connected {
+ self?.reconnectIfNeeded()
+ }
}
- networkMonitor.startMonitoring()
+ .store(in: &publishers)
}
private func registerBackgroundTask() {
diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
index 924721f16..3071e12e6 100644
--- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
+++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
@@ -298,6 +298,8 @@ private extension ApproveEngine {
}
proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey)
+ pairingRegisterer.setReceived(pairingTopic: payload.topic)
+
Task(priority: .high) {
let assertionId = payload.decryptedPayload.sha256().toHexString()
do {
diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift
index 039cccecf..2ab3267ae 100644
--- a/Sources/WalletConnectVerify/OriginVerifier.swift
+++ b/Sources/WalletConnectVerify/OriginVerifier.swift
@@ -5,22 +5,44 @@ public final class OriginVerifier {
case registrationFailed
}
- private let verifyHost: String
+ private var verifyHost: String
+ /// The property is used to determine whether verify.walletconnect.org will be used
+ /// in case verify.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location).
+ private var fallback = false
init(verifyHost: String) {
self.verifyHost = verifyHost
}
func verifyOrigin(assertionId: String) async throws -> String {
- let httpClient = HTTPNetworkClient(host: verifyHost)
- let response = try await httpClient.request(
- VerifyResponse.self,
- at: VerifyAPI.resolve(assertionId: assertionId)
- )
- guard let origin = response.origin else {
- throw Errors.registrationFailed
+ let sessionConfiguration = URLSessionConfiguration.default
+ sessionConfiguration.timeoutIntervalForRequest = 5.0
+ sessionConfiguration.timeoutIntervalForResource = 5.0
+ let session = URLSession(configuration: sessionConfiguration)
+
+ let httpClient = HTTPNetworkClient(host: verifyHost, session: session)
+
+ do {
+ let response = try await httpClient.request(
+ VerifyResponse.self,
+ at: VerifyAPI.resolve(assertionId: assertionId)
+ )
+ guard let origin = response.origin else {
+ throw Errors.registrationFailed
+ }
+ return origin
+ } catch {
+ if (error as? HTTPError) == .couldNotConnect && !fallback {
+ fallback = true
+ verifyHostFallback()
+ return try await verifyOrigin(assertionId: assertionId)
+ }
+ throw error
}
- return origin
+ }
+
+ func verifyHostFallback() {
+ verifyHost = "verify.walletconnect.org"
}
}
diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift
index 92c0895af..12f7c1d94 100644
--- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift
+++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift
@@ -24,7 +24,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase {
func testConnectsOnConnectionSatisfied() {
webSocketSession.disconnect()
XCTAssertFalse(webSocketSession.isConnected)
- networkMonitor.onSatisfied?()
+ networkMonitor.networkConnectionStatusPublisherSubject.send(.connected)
XCTAssertTrue(webSocketSession.isConnected)
}
diff --git a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift
index 7f6a5245e..1095d1677 100644
--- a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift
+++ b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift
@@ -1,9 +1,14 @@
import Foundation
+import Combine
+
@testable import WalletConnectRelay
class NetworkMonitoringMock: NetworkMonitoring {
- var onSatisfied: (() -> Void)?
- var onUnsatisfied: (() -> Void)?
-
- func startMonitoring() { }
+ var networkConnectionStatusPublisher: AnyPublisher {
+ networkConnectionStatusPublisherSubject.eraseToAnyPublisher()
+ }
+
+ let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected)
+
+ public init() { }
}
diff --git a/Tests/TestingUtils/Mocks/HTTPClientMock.swift b/Tests/TestingUtils/Mocks/HTTPClientMock.swift
index 4746bed92..5593b4096 100644
--- a/Tests/TestingUtils/Mocks/HTTPClientMock.swift
+++ b/Tests/TestingUtils/Mocks/HTTPClientMock.swift
@@ -2,7 +2,6 @@ import Foundation
@testable import HTTPClient
public final class HTTPClientMock: HTTPClient {
-
private let object: T
public init(object: T) {
@@ -16,4 +15,8 @@ public final class HTTPClientMock: HTTPClient {
public func request(service: HTTPService) async throws {
}
+
+ public func updateHost(host: String) async {
+
+ }
}
diff --git a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift
index ee0cfec9b..6aa18183f 100644
--- a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift
+++ b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift
@@ -4,7 +4,6 @@ import Combine
import WalletConnectNetworking
public class PairingRegistererMock: PairingRegisterer where RequestParams: Codable {
-
public let subject = PassthroughSubject, Never>()
public var isActivateCalled: Bool = false
@@ -20,4 +19,8 @@ public class PairingRegistererMock: PairingRegisterer where Reque
public func validatePairingExistance(_ topic: String) throws {
}
+
+ public func setReceived(pairingTopic: String) {
+
+ }
}
diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift
index 14f7f9d24..175e6aed0 100644
--- a/Tests/TestingUtils/NetworkingInteractorMock.swift
+++ b/Tests/TestingUtils/NetworkingInteractorMock.swift
@@ -25,9 +25,14 @@ public class NetworkingInteractorMock: NetworkInteracting {
var onRespondError: ((Int) -> Void)?
public let socketConnectionStatusPublisherSubject = PassthroughSubject()
+ public let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected)
+
public var socketConnectionStatusPublisher: AnyPublisher {
socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
}
+ public var networkConnectionStatusPublisher: AnyPublisher {
+ networkConnectionStatusPublisherSubject.eraseToAnyPublisher()
+ }
public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never>()
public let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse, publishedAt: Date, derivedTopic: String?), Never>()
diff --git a/Tests/WalletConnectModalTests/ModalViewModelTests.swift b/Tests/WalletConnectModalTests/ModalViewModelTests.swift
index 6372b6dcc..55de25cc9 100644
--- a/Tests/WalletConnectModalTests/ModalViewModelTests.swift
+++ b/Tests/WalletConnectModalTests/ModalViewModelTests.swift
@@ -18,11 +18,47 @@ final class ModalViewModelTests: XCTestCase {
sut = .init(
isShown: .constant(true),
interactor: ModalSheetInteractorMock(listings: [
- Listing(id: "1", name: "Sample App", homepage: "https://example.com", order: 1, imageId: "1", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: nil, universal: "https://example.com/universal")),
- Listing(id: "2", name: "Awesome App", homepage: "https://example.com/awesome", order: 2, imageId: "2", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "awesomeapp://deeplink", universal: "https://awesome.com/awesome/universal")),
+ Listing(
+ id: "1",
+ name: "Sample App",
+ homepage: "https://example.com",
+ order: 1,
+ imageId: "1",
+ app: Listing.App(
+ ios: "https://example.com/download-ios",
+ browser: "https://example.com/wallet"
+ ),
+ mobile: Listing.Links(
+ native: nil,
+ universal: "https://example.com/universal"
+ ),
+ desktop: Listing.Links(
+ native: nil,
+ universal: "https://example.com/universal"
+ )
+ ),
+ Listing(
+ id: "2",
+ name: "Awesome App",
+ homepage: "https://example.com/awesome",
+ order: 2,
+ imageId: "2",
+ app: Listing.App(
+ ios: "https://example.com/download-ios",
+ browser: "https://example.com/wallet"
+ ),
+ mobile: Listing.Links(
+ native: "awesomeapp://deeplink",
+ universal: "https://awesome.com/awesome/universal"
+ ),
+ desktop: Listing.Links(
+ native: "awesomeapp://deeplink",
+ universal: "https://awesome.com/awesome/desktop/universal"
+ )
+ ),
]),
uiApplicationWrapper: .init(
- openURL: { url, _ in
+ openURL: { url, _ in
self.openURLFuncTest.call(url)
self.expectation.fulfill()
},
@@ -53,8 +89,7 @@ final class ModalViewModelTests: XCTestCase {
expectation = XCTestExpectation(description: "Wait for openUrl to be called")
- sut.onListingTap(sut.wallets[0], preferUniversal: true)
-
+ sut.navigateToDeepLink(wallet: sut.wallets[0], preferUniversal: true, preferBrowser: false)
XCTWaiter.wait(for: [expectation], timeout: 3)
XCTAssertEqual(
@@ -64,8 +99,7 @@ final class ModalViewModelTests: XCTestCase {
expectation = XCTestExpectation(description: "Wait for openUrl to be called using universal link")
- sut.onListingTap(sut.wallets[1], preferUniversal: false)
-
+ sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: false)
XCTWaiter.wait(for: [expectation], timeout: 3)
XCTAssertEqual(
@@ -73,16 +107,24 @@ final class ModalViewModelTests: XCTestCase {
URL(string: "awesomeapp://deeplinkwc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")!
)
-
expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link")
- sut.onListingTap(sut.wallets[1], preferUniversal: true)
-
+ sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: true, preferBrowser: false)
XCTWaiter.wait(for: [expectation], timeout: 3)
XCTAssertEqual(
openURLFuncTest.currentValue,
URL(string: "https://awesome.com/awesome/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")!
)
+
+ expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link")
+
+ sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: true)
+ XCTWaiter.wait(for: [expectation], timeout: 3)
+
+ XCTAssertEqual(
+ openURLFuncTest.currentValue,
+ URL(string: "https://awesome.com/awesome/desktop/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")!
+ )
}
}
diff --git a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
index 7617aee57..facccf6b4 100644
--- a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
+++ b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
@@ -18,16 +18,20 @@ final class WalletPairServiceTestsTests: XCTestCase {
cryptoMock = KeyManagementServiceMock()
service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock)
}
+
+ func testPairWhenNetworkNotConnectedThrows() async {
+ let uri = WalletConnectURI.stub()
+ networkingInteractor.networkConnectionStatusPublisherSubject.send(.notConnected)
+ await XCTAssertThrowsErrorAsync(try await service.pair(uri))
+ }
- func testPairMultipleTimesOnSameURIThrows() async {
+ func testPairOnSameURIWhenRequestReceivedThrows() async {
let uri = WalletConnectURI.stub()
- for i in 1...10 {
- if i == 1 {
- await XCTAssertNoThrowAsync(try await service.pair(uri))
- } else {
- await XCTAssertThrowsErrorAsync(try await service.pair(uri))
- }
- }
+ try! await service.pair(uri)
+ var pairing = storageMock.getPairing(forTopic: uri.topic)
+ pairing?.receivedRequest()
+ storageMock.setPairing(pairing!)
+ await XCTAssertThrowsErrorAsync(try await service.pair(uri))
}
func testPair() async {