diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 3b72f0969..b92d3f2a0 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0D828E436A3001BACF9 /* InputConfig.swift */; }; A51AC0DD28E43727001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DB28E436E6001BACF9 /* InputConfig.swift */; }; A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DE28E4379F001BACF9 /* InputConfig.swift */; }; + A5417BBE299BFC3E00B469F3 /* ImportAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */; }; A541959E2934BFEF0035AD19 /* CacaoSignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */; }; A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959B2934BFEF0035AD19 /* SignerTests.swift */; }; A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */; }; @@ -126,7 +127,6 @@ A5629AE828772A0100094373 /* InviteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AE728772A0100094373 /* InviteViewModel.swift */; }; A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AE92877F2D600094373 /* WalletConnectChat */; }; A5629AF22877F75100094373 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AF12877F75100094373 /* Starscream */; }; - A574B3582964560C00C2BB91 /* Web3Inbox in Frameworks */ = {isa = PBXBuildFile; productRef = A574B3572964560C00C2BB91 /* Web3Inbox */; }; A578FA322873036400AA7720 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA312873036400AA7720 /* InputView.swift */; }; A578FA35287304A300AA7720 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA34287304A300AA7720 /* Color.swift */; }; A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA362873D8EE00AA7720 /* UIColor.swift */; }; @@ -160,6 +160,9 @@ A58E7D432872EE320082D443 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D422872EE320082D443 /* MessageView.swift */; }; A58E7D452872EE570082D443 /* ContentMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D442872EE570082D443 /* ContentMessageView.swift */; }; A58E7D482872EF610082D443 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D472872EF610082D443 /* MessageViewModel.swift */; }; + A58EC611299D57B800F3452A /* AsyncButton in Frameworks */ = {isa = PBXBuildFile; productRef = A58EC610299D57B800F3452A /* AsyncButton */; }; + A58EC616299D5C6400F3452A /* PlainButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58EC615299D5C6400F3452A /* PlainButton.swift */; }; + A58EC618299D665A00F3452A /* Web3Inbox in Frameworks */ = {isa = PBXBuildFile; productRef = A58EC617299D665A00F3452A /* Web3Inbox */; }; A59CF4F6292F83D50031A42F /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A59F877528B5462900A9CD80 /* WalletConnectAuth */; }; A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A59FAEC828B7B93A002BB66F /* Web3 */; }; @@ -193,10 +196,8 @@ A5C2021D287E1FD8007E3188 /* ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20218287E1FD8007E3188 /* ImportView.swift */; }; A5C20221287EA5B8007E3188 /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20220287EA5B8007E3188 /* TextFieldView.swift */; }; A5C20223287EA7E2007E3188 /* BrandButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20222287EA7E2007E3188 /* BrandButton.swift */; }; - A5C20226287EB099007E3188 /* AccountNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20225287EB099007E3188 /* AccountNameResolver.swift */; }; A5C20229287EB34C007E3188 /* AccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20228287EB34C007E3188 /* AccountStorage.swift */; }; A5C2022B287EB89A007E3188 /* WelcomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C2022A287EB89A007E3188 /* WelcomeInteractor.swift */; }; - A5C2022D287EC3F0007E3188 /* RegisterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C2022C287EC3F0007E3188 /* RegisterService.swift */; }; A5C4DD8728A2DE88006A626D /* WalletConnectRouter in Frameworks */ = {isa = PBXBuildFile; productRef = A5C4DD8628A2DE88006A626D /* WalletConnectRouter */; }; A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A5C8BE84292FE20B006CC85C /* Web3 */; }; A5D85226286333D500DAF5C3 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5D85225286333D500DAF5C3 /* Starscream */; }; @@ -449,6 +450,7 @@ A51AC0D828E436A3001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; A51AC0DB28E436E6001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; A51AC0DE28E4379F001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; + A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportAccount.swift; sourceTree = ""; }; A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacaoSignerTests.swift; sourceTree = ""; }; A541959B2934BFEF0035AD19 /* SignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = ""; }; A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP1271VerifierTests.swift; sourceTree = ""; }; @@ -507,6 +509,7 @@ A58E7D422872EE320082D443 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; A58E7D442872EE570082D443 /* ContentMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMessageView.swift; sourceTree = ""; }; A58E7D472872EF610082D443 /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; + A58EC615299D5C6400F3452A /* PlainButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainButton.swift; sourceTree = ""; }; A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSignerFactory.swift; sourceTree = ""; }; A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = ""; }; A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewModel.swift; sourceTree = ""; }; @@ -531,10 +534,8 @@ A5C20218287E1FD8007E3188 /* ImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportView.swift; sourceTree = ""; }; A5C20220287EA5B8007E3188 /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; A5C20222287EA7E2007E3188 /* BrandButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrandButton.swift; sourceTree = ""; }; - A5C20225287EB099007E3188 /* AccountNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNameResolver.swift; sourceTree = ""; }; A5C20228287EB34C007E3188 /* AccountStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStorage.swift; sourceTree = ""; }; A5C2022A287EB89A007E3188 /* WelcomeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeInteractor.swift; sourceTree = ""; }; - A5C2022C287EC3F0007E3188 /* RegisterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterService.swift; sourceTree = ""; }; A5E03DED286464DB00888481 /* IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A5E03DF9286465C700888481 /* SignClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignClientTests.swift; sourceTree = ""; }; A5E03DFC286465D100888481 /* Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stubs.swift; sourceTree = ""; }; @@ -664,11 +665,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A58EC618299D665A00F3452A /* Web3Inbox in Frameworks */, A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */, A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */, A5629AF22877F75100094373 /* Starscream in Frameworks */, - A574B3582964560C00C2BB91 /* Web3Inbox in Frameworks */, A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */, + A58EC611299D57B800F3452A /* AsyncButton in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1104,9 +1106,8 @@ A5629AEB2877F69C00094373 /* Chat */ = { isa = PBXGroup; children = ( + A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */, A5629AA82876A23100094373 /* ChatService.swift */, - A5C20225287EB099007E3188 /* AccountNameResolver.swift */, - A5C2022C287EC3F0007E3188 /* RegisterService.swift */, ); path = Chat; sourceTree = ""; @@ -1409,6 +1410,7 @@ A578FA312873036400AA7720 /* InputView.swift */, A5C20220287EA5B8007E3188 /* TextFieldView.swift */, A5C20222287EA7E2007E3188 /* BrandButton.swift */, + A58EC615299D5C6400F3452A /* PlainButton.swift */, ); path = Components; sourceTree = ""; @@ -1860,7 +1862,8 @@ A5629AF12877F75100094373 /* Starscream */, A59F877528B5462900A9CD80 /* WalletConnectAuth */, A59FAEC828B7B93A002BB66F /* Web3 */, - A574B3572964560C00C2BB91 /* Web3Inbox */, + A58EC610299D57B800F3452A /* AsyncButton */, + A58EC617299D665A00F3452A /* Web3Inbox */, ); productName = Showcase; productReference = A58E7CE828729F550082D443 /* Showcase.app */; @@ -1987,6 +1990,7 @@ A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */, A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */, A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */, + A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -2154,13 +2158,13 @@ buildActionMask = 2147483647; files = ( A58E7D3B2872D55F0082D443 /* ChatInteractor.swift in Sources */, + A5417BBE299BFC3E00B469F3 /* ImportAccount.swift in Sources */, A58E7D1F2872A57B0082D443 /* ApplicationConfigurator.swift in Sources */, A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */, A58E7D452872EE570082D443 /* ContentMessageView.swift in Sources */, A5C20223287EA7E2007E3188 /* BrandButton.swift in Sources */, A5629ADF2876CC6E00094373 /* InviteListPresenter.swift in Sources */, A58E7D392872D55F0082D443 /* ChatModule.swift in Sources */, - A5C2022D287EC3F0007E3188 /* RegisterService.swift in Sources */, A58E7D222872A57B0082D443 /* AppearanceConfigurator.swift in Sources */, A50F3946288005B200064555 /* Types.swift in Sources */, A58E7D212872A57B0082D443 /* MigrationConfigurator.swift in Sources */, @@ -2168,6 +2172,7 @@ A58E7D0E2872A45B0082D443 /* MainRouter.swift in Sources */, A58E7D432872EE320082D443 /* MessageView.swift in Sources */, A58E7D3F2872E99A0082D443 /* TabPage.swift in Sources */, + A58EC616299D5C6400F3452A /* PlainButton.swift in Sources */, A58E7D3C2872D55F0082D443 /* ChatPresenter.swift in Sources */, A5C2020B287D9DEE007E3188 /* WelcomeModule.swift in Sources */, A58E7D152872A5410082D443 /* UIViewController.swift in Sources */, @@ -2194,7 +2199,6 @@ A58E7CED28729F550082D443 /* SceneDelegate.swift in Sources */, A5C2020F287D9DEE007E3188 /* WelcomeView.swift in Sources */, A518A98929683FB60035247E /* Web3InboxRouter.swift in Sources */, - A5C20226287EB099007E3188 /* AccountNameResolver.swift in Sources */, A5C2020D287D9DEE007E3188 /* WelcomeRouter.swift in Sources */, A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */, A518A98729683FB60035247E /* Web3InboxViewController.swift in Sources */, @@ -2810,7 +2814,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2844,7 +2848,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3103,6 +3107,14 @@ kind = branch; }; }; + A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/lorenzofiamingo/swiftui-async-button"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/WalletConnect/Web3.swift"; @@ -3185,7 +3197,12 @@ package = A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; - A574B3572964560C00C2BB91 /* Web3Inbox */ = { + A58EC610299D57B800F3452A /* AsyncButton */ = { + isa = XCSwiftPackageProductDependency; + package = A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */; + productName = AsyncButton; + }; + A58EC617299D665A00F3452A /* Web3Inbox */ = { isa = XCSwiftPackageProductDependency; productName = Web3Inbox; }; diff --git a/Example/IntegrationTests/Auth/Signer/SignerTests.swift b/Example/IntegrationTests/Auth/Signer/SignerTests.swift index 2a8f49b2d..568dd2c82 100644 --- a/Example/IntegrationTests/Auth/Signer/SignerTests.swift +++ b/Example/IntegrationTests/Auth/Signer/SignerTests.swift @@ -26,12 +26,12 @@ class SignerTest: XCTestCase { func testSignerAddressFromIss() throws { let iss = "did:pkh:eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07" - XCTAssertEqual(try DIDPKH(iss: iss).account, Account("eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")!) + XCTAssertEqual(try DIDPKH(did: iss).account, Account("eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")!) } func testSignerAddressFromAccount() throws { let account = Account("eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")! - XCTAssertEqual(DIDPKH(account: account).iss, "did:pkh:eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07") + XCTAssertEqual(DIDPKH(account: account).string, "did:pkh:eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07") } } diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index a10fb95e2..7024106ce 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -9,46 +9,54 @@ import Combine final class ChatTests: XCTestCase { var invitee: ChatClient! var inviter: ChatClient! - var registry: KeyValueRegistry! private var publishers = [AnyCancellable]() - let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! - let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! + let inviteeAccount = Account("eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688")! + let inviterAccount = Account("eip155:2:0x15bca56b6e2728aec2532df9d436bd1600e86688")! - override func setUp() { - registry = KeyValueRegistry() - invitee = makeClient(prefix: "🦖 Registered", account: inviteeAccount) + let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") + + override func setUp() async throws { + invitee = makeClient(prefix: "🦖 Invitee", account: inviteeAccount) inviter = makeClient(prefix: "🍄 Inviter", account: inviterAccount) + + try await invitee.register(account: inviteeAccount, onSign: sign) + try await inviter.register(account: inviterAccount, onSign: sign) } func makeClient(prefix: String, account: Account) -> ChatClient { + let keyserverURL = URL(string: "https://staging.keys.walletconnect.com")! let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) let keychain = KeychainStorageMock() let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), logger: logger) - return ChatClientFactory.create(account: account, registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) + return ChatClientFactory.create(account: account, keyserverURL: keyserverURL, relayClient: relayClient, keychain: keychain, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) } - func testInvite() async { + func testInvite() async throws { let inviteExpectation = expectation(description: "invitation expectation") - try! await invitee.register(account: inviteeAccount) - try! await inviter.invite(peerAccount: inviteeAccount, openingMessage: "") - invitee.invitePublisher.sink { _ in + inviteExpectation.expectedFulfillmentCount = 2 + + invitee.newReceivedInvitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) + + inviter.newSentInvitePublisher.sink { _ in + inviteExpectation.fulfill() + }.store(in: &publishers) + + let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) + _ = try await inviter.invite(invite: invite) + wait(for: [inviteExpectation], timeout: InputConfig.defaultTimeout) } - func testAcceptAndCreateNewThread() { + func testAcceptAndCreateNewThread() async throws { let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation") let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation") - Task(priority: .high) { - try! await invitee.register(account: inviteeAccount) - try! await inviter.invite(peerAccount: inviteeAccount, openingMessage: "opening message") - } - - invitee.invitePublisher.sink { [unowned self] invite in - Task {try! await invitee.accept(inviteId: invite.id)} + invitee.newReceivedInvitePublisher.sink { [unowned self] invite in + Task { try! await invitee.accept(inviteId: invite.id) } }.store(in: &publishers) invitee.newThreadPublisher.sink { _ in @@ -59,38 +67,57 @@ final class ChatTests: XCTestCase { newThreadInviterExpectation.fulfill() }.store(in: &publishers) + let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) + try await inviter.invite(invite: invite) + wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: InputConfig.defaultTimeout) } - func testMessage() { + func testMessage() async throws { let messageExpectation = expectation(description: "message received") - messageExpectation.expectedFulfillmentCount = 4 // expectedFulfillmentCount 4 because onMessage() called on send too - - Task(priority: .high) { - try! await invitee.register(account: inviteeAccount) - try! await inviter.invite(peerAccount: inviteeAccount, openingMessage: "opening message") - } + messageExpectation.expectedFulfillmentCount = 4 - invitee.invitePublisher.sink { [unowned self] invite in - Task {try! await invitee.accept(inviteId: invite.id)} + invitee.newReceivedInvitePublisher.sink { [unowned self] invite in + Task { try! await invitee.accept(inviteId: invite.id) } }.store(in: &publishers) invitee.newThreadPublisher.sink { [unowned self] thread in - Task {try! await invitee.message(topic: thread.topic, message: "message")} + Task { try! await invitee.message(topic: thread.topic, message: "message1") } }.store(in: &publishers) inviter.newThreadPublisher.sink { [unowned self] thread in - Task {try! await inviter.message(topic: thread.topic, message: "message")} + Task { try! await inviter.message(topic: thread.topic, message: "message2") } }.store(in: &publishers) - inviter.messagePublisher.sink { _ in + inviter.newMessagePublisher.sink { message in + if message.authorAccount == self.inviterAccount { + XCTAssertEqual(message.message, "message2") + } else { + XCTAssertEqual(message.message, "message1") + } messageExpectation.fulfill() }.store(in: &publishers) - invitee.messagePublisher.sink { _ in + invitee.newMessagePublisher.sink { message in + if message.authorAccount == self.inviteeAccount { + XCTAssertEqual(message.message, "message1") + } else { + XCTAssertEqual(message.message, "message2") + } messageExpectation.fulfill() }.store(in: &publishers) + let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) + try await inviter.invite(invite: invite) + wait(for: [messageExpectation], timeout: InputConfig.defaultTimeout) } + + private func sign(_ message: String) -> SigningResult { + let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) + return .signed(try! signer.sign(message: message, privateKey: privateKey, type: .eip191)) + } } diff --git a/Example/IntegrationTests/Chat/RegistryTests.swift b/Example/IntegrationTests/Chat/RegistryTests.swift index e2f58588c..80019edaf 100644 --- a/Example/IntegrationTests/Chat/RegistryTests.swift +++ b/Example/IntegrationTests/Chat/RegistryTests.swift @@ -9,7 +9,7 @@ final class RegistryTests: XCTestCase { let account = Account("eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688")! let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") - var sut: IdentityRegisterService! + var sut: IdentityService! var storage: IdentityStorage! var signer: CacaoMessageSigner! @@ -18,11 +18,14 @@ final class RegistryTests: XCTestCase { let httpService = HTTPNetworkClient(host: keyserverURL.host!) let accountService = AccountService(currentAccount: account) let identityNetworkService = IdentityNetworkService(accountService: accountService, httpService: httpService) - storage = IdentityStorage(keychain: KeychainStorageMock()) - sut = IdentityRegisterService( + let keychain = KeychainStorageMock() + let ksm = KeyManagementService(keychain: keychain) + storage = IdentityStorage(keychain: keychain) + sut = IdentityService( keyserverURL: keyserverURL, - identityStorage: storage, - identityNetworkService: identityNetworkService, + kms: ksm, + storage: storage, + networkService: identityNetworkService, iatProvader: DefaultIATProvider(), messageFormatter: SIWECacaoFormatter() ) @@ -30,26 +33,32 @@ final class RegistryTests: XCTestCase { } func testRegisterIdentityAndInviteKey() async throws { - var message: String! - let publicKey = try await sut.registerIdentity(account: account, isPrivate: false) { msg in - message = msg - return try! signer.sign(message: msg, privateKey: privateKey, type: .eip191) - } + let publicKey = try await sut.registerIdentity(account: account, onSign: onSign) - let cacao = try await sut.resolveIdentity(publicKey: publicKey) - XCTAssertEqual(try SIWECacaoFormatter().formatMessage(from: cacao.p), message) + let iss = DIDKey(rawData: Data(hex: publicKey)).did(prefix: true, variant: .ED25519) + let resolvedAccount = try await sut.resolveIdentity(iss: iss) + XCTAssertEqual(resolvedAccount, account) let recovered = storage.getIdentityKey(for: account)!.publicKey.hexRepresentation XCTAssertEqual(publicKey, recovered) - let inviteKey = try await sut.registerInvite(account: account, isPrivate: false, onSign: { msg in - return try! signer.sign(message: msg, privateKey: privateKey, type: .eip191) - }) + let inviteKey = try await sut.registerInvite(account: account) let recoveredKey = storage.getInviteKey(for: account)! - XCTAssertEqual(inviteKey, recoveredKey.publicKey.hexRepresentation) + XCTAssertEqual(inviteKey, recoveredKey) let resolvedKey = try await sut.resolveInvite(account: account) - XCTAssertEqual(inviteKey, resolvedKey) + XCTAssertEqual(inviteKey.did, resolvedKey) + + _ = try await sut.goPrivate(account: account) + try await sut.unregister(account: account, onSign: onSign) + } +} + +private extension RegistryTests { + + func onSign(_ message: String) -> SigningResult { + let signature = try! signer.sign(message: message, privateKey: privateKey, type: .eip191) + return .signed(signature) } } diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift index 7feb9244b..4f4917dea 100644 --- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift +++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift @@ -10,11 +10,9 @@ final class RelayClientEndToEndTests: XCTestCase { private var publishers = Set() func makeRelayClient() -> RelayClient { - let didKeyFactory = ED25519DIDKeyFactory() - let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock(), didKeyFactory: didKeyFactory) + let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) let socketAuthenticator = SocketAuthenticator( clientIdStorage: clientIdStorage, - didKeyFactory: didKeyFactory, relayHost: InputConfig.relayHost ) let urlFactory = RelayUrlFactory(socketAuthenticator: socketAuthenticator) @@ -62,12 +60,12 @@ final class RelayClientEndToEndTests: XCTestCase { expectationA.assertForOverFulfill = false expectationB.assertForOverFulfill = false - relayA.messagePublisher.sink { topic, payload in + relayA.messagePublisher.sink { topic, payload, _ in (subscriptionATopic, subscriptionAPayload) = (topic, payload) expectationA.fulfill() }.store(in: &publishers) - relayB.messagePublisher.sink { topic, payload in + relayB.messagePublisher.sink { topic, payload, _ in (subscriptionBTopic, subscriptionBPayload) = (topic, payload) expectationB.fulfill() }.store(in: &publishers) diff --git a/Example/Showcase/Classes/ApplicationLayer/Application.swift b/Example/Showcase/Classes/ApplicationLayer/Application.swift index 46b30ef87..27a3577f4 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Application.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Application.swift @@ -10,8 +10,4 @@ final class Application { lazy var accountStorage: AccountStorage = { return AccountStorage(defaults: .standard) }() - - lazy var registerService: RegisterService = { - return RegisterService(chatService: chatService) - }() } diff --git a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift index ab86a093a..c8bfc44db 100644 --- a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift +++ b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift @@ -7,15 +7,32 @@ final class AccountStorage { self.defaults = defaults } - var account: Account? { + var importAccount: ImportAccount? { get { guard let value = UserDefaults.standard.string(forKey: "account") else { return nil } - return Account(value) + guard let account = ImportAccount(input: value) else { + // Migration + self.importAccount = nil + return nil + } + return account } set { - UserDefaults.standard.set(newValue?.absoluteString, forKey: "account") + UserDefaults.standard.set(newValue?.storageId, forKey: "account") + } + } +} + +private extension ImportAccount { + + var storageId: String { + switch self { + case .swift, .kotlin, .js: + return name + case .custom(let privateKey): + return privateKey } } } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/AccountNameResolver.swift b/Example/Showcase/Classes/DomainLayer/Chat/AccountNameResolver.swift deleted file mode 100644 index dc7ac4339..000000000 --- a/Example/Showcase/Classes/DomainLayer/Chat/AccountNameResolver.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -struct AccountNameResolver { - - private static var staticMap: [String: String] = [ - "swift.eth": "eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", - "kotlin.eth": "eip155:2:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb", - "js.eth": "eip155:3:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb" - ] - - static func resolveName(_ account: Account) -> String { - return staticMap - .first(where: { $0.value == account.absoluteString })?.key ?? account.absoluteString - } - - static func resolveAccount(_ input: String) -> Account? { - guard let value = staticMap[input.lowercased()] else { - return Account(input) - } - - return Account(value)! - } -} diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 839ef53a6..ca1843015 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -3,15 +3,15 @@ import Combine import WalletConnectChat import WalletConnectRelay -typealias Stream = AsyncPublisher> +typealias Stream = AnyPublisher final class ChatService { private lazy var client: ChatClient = { - guard let account = accountStorage.account else { + guard let importAccount = accountStorage.importAccount else { fatalError("Error - you must call Chat.configure(_:) before accessing the shared instance.") } - Chat.configure(account: account) + Chat.configure(account: importAccount.account) return Chat.instance }() @@ -26,19 +26,36 @@ final class ChatService { } var connectionPublisher: Stream { - return networking.socketConnectionStatusPublisher.values + return networking.socketConnectionStatusPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } - var messagePublisher: Stream { - return client.messagePublisher.values + var threadPublisher: Stream<[WalletConnectChat.Thread]> { + return client.threadsPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } - var threadPublisher: Stream { - return client.newThreadPublisher.values + var receivedInvitePublisher: Stream<[ReceivedInvite]> { + return client.receivedInvitesPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } - var invitePublisher: Stream { - return client.invitePublisher.values + var sentInvitePublisher: Stream<[SentInvite]> { + return client.sentInvitesPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func messagePublisher(thread: WalletConnectChat.Thread) -> Stream<[Message]> { + return client.messagesPublisher + .map { + $0.filter { $0.topic == thread.topic } + } + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } func getMessages(thread: WalletConnectChat.Thread) -> [WalletConnectChat.Message] { @@ -49,31 +66,60 @@ final class ChatService { client.getThreads() } - func getInvites() -> [WalletConnectChat.Invite] { - client.getInvites() + func getReceivedInvites() -> [ReceivedInvite] { + client.getReceivedInvites() } func sendMessage(topic: String, message: String) async throws { try await client.message(topic: topic, message: message) } - func accept(invite: Invite) async throws { + func accept(invite: ReceivedInvite) async throws { try await client.accept(inviteId: invite.id) } - func reject(invite: Invite) async throws { + func reject(invite: ReceivedInvite) async throws { try await client.reject(inviteId: invite.id) } - func invite(peerAccount: Account, message: String) async throws { - try await client.invite(peerAccount: peerAccount, openingMessage: message) + func goPublic(account: Account, privateKey: String) async throws { + try await client.goPublic(account: account) + } + + func invite(inviterAccount: Account, inviteeAccount: Account, message: String) async throws { + let inviteePublicKey = try await client.resolve(account: inviteeAccount) + let invite = Invite(message: message, inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) + try await client.invite(invite: invite) + } + + func register(account: Account, privateKey: String) async throws { + _ = try await client.register(account: account) { message in + let signature = self.onSign(message: message, privateKey: privateKey) + return SigningResult.signed(signature) + } + } + + func unregister(account: Account, privateKey: String) async throws { + try await client.unregister(account: account) { message in + let signature = self.onSign(message: message, privateKey: privateKey) + return SigningResult.signed(signature) + } } - func register(account: Account) async throws { - _ = try await client.register(account: account) + func goPrivate(account: Account) async throws { + try await client.goPrivate(account: account) } func resolve(account: Account) async throws -> String { return try await client.resolve(account: account) } } + +private extension ChatService { + + func onSign(message: String, privateKey: String) -> CacaoSignature { + let privateKey = Data(hex: privateKey) + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() + return try! signer.sign(message: message, privateKey: privateKey, type: .eip191) + } +} diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift new file mode 100644 index 000000000..329f30e75 --- /dev/null +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -0,0 +1,68 @@ +import Foundation +import Web3 + +enum ImportAccount { + case swift + case kotlin + case js + case custom(privateKey: String) + + init?(input: String) { + switch input.lowercased() { + case ImportAccount.swift.name: + self = .swift + case ImportAccount.kotlin.name: + self = .kotlin + case ImportAccount.js.name: + self = .js + default: + if let _ = try? EthereumPrivateKey(hexPrivateKey: "0x" + input, ctx: nil) { + self = .custom(privateKey: input) + } else if let _ = try? EthereumPrivateKey(hexPrivateKey: input, ctx: nil) { + self = .custom(privateKey: input.replacingOccurrences(of: "0x", with: "")) + } else { + return nil + } + } + } + + var name: String { + switch self { + case .swift: + return "swift.eth" + case .kotlin: + return "kotlin.eth" + case .js: + return "js.eth" + case .custom: + return account.address + } + } + + var account: Account { + switch self { + case .swift: + return Account("eip155:1:0x1AAe9864337E821f2F86b5D27468C59AA333C877")! + case .kotlin: + return Account("eip155:1:0x4c0fb06CD854ab7D5909E830a5f49D184EB41BF5")! + case .js: + return Account("eip155:1:0x7ABa5B1F436e42f6d4A579FB3Ad6D204F6A91863")! + case .custom(let privateKey): + let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.rawAddress + return Account("eip155:1:\(address))")! + } + } + + var privateKey: String { + switch self { + case .swift: + return "4dc0055d1831f7df8d855fc8cd9118f4a85ddc05395104c4cb0831a6752621a8" + case .kotlin: + return "ebe738a76b9a3b7457c3d5eca8d3d9ea6909bc563e05b6e0c5c35448f93100a0" + case .js: + return "de15cb11963e9bde0a5cce06a5ee2bda1cf3a67be6fbcd7a4fc8c0e4c4db0298" + case .custom(let privateKey): + return privateKey + } + } +} diff --git a/Example/Showcase/Classes/DomainLayer/Chat/RegisterService.swift b/Example/Showcase/Classes/DomainLayer/Chat/RegisterService.swift deleted file mode 100644 index 881c94b28..000000000 --- a/Example/Showcase/Classes/DomainLayer/Chat/RegisterService.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -final class RegisterService { - - private let chatService: ChatService - - init(chatService: ChatService) { - self.chatService = chatService - } - - func register(account: Account) async { - try! await chatService.register(account: account) - print("Account: \(account.absoluteString) registered") - } -} diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift index 466b2d102..dab663f14 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift @@ -9,12 +9,12 @@ final class ChatInteractor { self.chatService = chatService } - func getMessages(thread: WalletConnectChat.Thread) async -> [Message] { - return await chatService.getMessages(thread: thread) + func getMessages(thread: WalletConnectChat.Thread) -> [Message] { + return chatService.getMessages(thread: thread) } - func messagesSubscription() -> Stream { - return chatService.messagePublisher + func messagesSubscription(thread: WalletConnectChat.Thread) -> Stream<[Message]> { + return chatService.messagePublisher(thread: thread) } func sendMessage(topic: String, message: String) async throws { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift index 3012bb953..e19db581b 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift @@ -9,27 +9,24 @@ final class ChatPresenter: ObservableObject { private let router: ChatRouter private var disposeBag = Set() - @Published var messages: [MessageViewModel] = [] + @Published private var messages: [Message] = [] @Published var input: String = .empty + var messageViewModels: [MessageViewModel] { + return messages.sorted(by: { $0.timestamp < $1.timestamp }) + .map { MessageViewModel(message: $0, thread: thread) } + } + init(thread: WalletConnectChat.Thread, interactor: ChatInteractor, router: ChatRouter) { + defer { setupInitialState() } self.thread = thread self.interactor = interactor self.router = router } - @MainActor - func setupInitialState() async { - await loadMessages() - - for await _ in interactor.messagesSubscription() { - await loadMessages() - } - } - func didPressSend() { Task(priority: .userInitiated) { - await sendMessage() + try await sendMessage() } } } @@ -39,7 +36,7 @@ final class ChatPresenter: ObservableObject { extension ChatPresenter: SceneViewModel { var sceneTitle: String? { - return AccountNameResolver.resolveName(thread.peerAccount) + return thread.peerAccount.address } } @@ -47,15 +44,18 @@ extension ChatPresenter: SceneViewModel { private extension ChatPresenter { - func loadMessages() async { - let messages = await interactor.getMessages(thread: thread) - self.messages = messages.sorted(by: { $0.timestamp < $1.timestamp }) - .map { MessageViewModel(message: $0, thread: thread) } + func setupInitialState() { + messages = interactor.getMessages(thread: thread) + + interactor.messagesSubscription(thread: thread) + .sink { [unowned self] messages in + self.messages = messages + }.store(in: &disposeBag) } @MainActor - func sendMessage() async { - try! await interactor.sendMessage(topic: thread.topic, message: input) + func sendMessage() async throws { + try await interactor.sendMessage(topic: thread.topic, message: input) input = .empty } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift index a7c0f2cd6..279a30a99 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift @@ -7,8 +7,7 @@ struct ChatView: View { var body: some View { ZStack { ChatScrollView { - // TODO: Replace id - ForEach(presenter.messages, id: \.text) { message in + ForEach(presenter.messageViewModels) { message in MessageView(message: message) } @@ -26,9 +25,6 @@ struct ChatView: View { } } } - .task { - await presenter.setupInitialState() - } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift index 538bd1bd7..291994f52 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift @@ -1,10 +1,14 @@ import Foundation import WalletConnectChat -struct MessageViewModel { +struct MessageViewModel: Identifiable { private let message: Message private let thread: WalletConnectChat.Thread + var id: Int64 { + return message.timestamp + } + init(message: Message, thread: WalletConnectChat.Thread) { self.message = message self.thread = thread diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift index 09a0c976a..20eee450e 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift @@ -14,19 +14,22 @@ final class ChatListInteractor { return chatService.getThreads() } - func threadsSubscription() -> Stream { + func threadsSubscription() -> Stream<[WalletConnectChat.Thread]> { return chatService.threadPublisher } - func getInvites() -> [Invite] { - return chatService.getInvites() + func getInvites() -> [ReceivedInvite] { + return chatService.getReceivedInvites() } - func invitesSubscription() -> Stream { - return chatService.invitePublisher + func receivedInvitesSubscription() -> Stream<[ReceivedInvite]> { + return chatService.receivedInvitePublisher } - func logout() { - accountStorage.account = nil + func logout() async throws { + guard let importAccount = accountStorage.importAccount else { return } + try await chatService.goPrivate(account: importAccount.account) + try await chatService.unregister(account: importAccount.account, privateKey: importAccount.privateKey) + accountStorage.importAccount = nil } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift index 0f7a0b412..f643271ab 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import WalletConnectChat final class ChatListPresenter: ObservableObject { @@ -8,30 +9,35 @@ final class ChatListPresenter: ObservableObject { private let account: Account private var disposeBag = Set() - @Published var threads: [ThreadViewModel] = [] - @Published var invites: [InviteViewModel] = [] + @Published private var threads: [WalletConnectChat.Thread] = [] + @Published private var receivedInvites: [ReceivedInvite] = [] + + var threadViewModels: [ThreadViewModel] { + return threads + .sorted(by: { $0.topic < $1.topic }) + .map { ThreadViewModel(thread: $0) } + } + + var inviteViewModels: [InviteViewModel] { + return receivedInvites + .filter { $0.status == .pending } + .sorted(by: { $0.timestamp < $1.timestamp }) + .map { InviteViewModel(invite: $0) } + } init(account: Account, interactor: ChatListInteractor, router: ChatListRouter) { + defer { setupInitialState() } self.account = account self.interactor = interactor self.router = router } - func setupInitialState() { - Task(priority: .userInitiated) { - await setupThreads() - } - Task(priority: .userInitiated) { - await setupInvites() - } - } - var requestsCount: String { - return String(invites.count) + return String(inviteViewModels.count) } var showRequests: Bool { - return !invites.isEmpty + return !inviteViewModels.isEmpty } func didPressThread(_ thread: ThreadViewModel) { @@ -42,8 +48,9 @@ final class ChatListPresenter: ObservableObject { router.presentInviteList(account: account) } - func didLogoutPress() { - interactor.logout() + @MainActor + func didLogoutPress() async throws { + try await interactor.logout() router.presentWelcome() } @@ -77,36 +84,19 @@ extension ChatListPresenter: SceneViewModel { private extension ChatListPresenter { - @MainActor - func setupThreads() async { - await loadThreads() - - for await _ in interactor.threadsSubscription() { - await loadThreads() - } - } - - @MainActor - func setupInvites() async { - loadInvites() - - for await _ in interactor.invitesSubscription() { - loadInvites() - } - } - - @MainActor - func loadThreads() async { - self.threads = interactor.getThreads() - .sorted(by: { $0.topic < $1.topic }) - .map { ThreadViewModel(thread: $0) } - } - - @MainActor - func loadInvites() { - self.invites = interactor.getInvites() - .sorted(by: { $0.publicKey < $1.publicKey }) - .map { InviteViewModel(invite: $0) } + func setupInitialState() { + threads = interactor.getThreads() + receivedInvites = interactor.getInvites() + + interactor.threadsSubscription() + .sink { [unowned self] threads in + self.threads = threads + }.store(in: &disposeBag) + + interactor.receivedInvitesSubscription() + .sink { [unowned self] receivedInvites in + self.receivedInvites = receivedInvites + }.store(in: &disposeBag) } @objc func presentInvite() { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift index 72b5f62f7..94644d90d 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift @@ -20,7 +20,7 @@ struct ChatListView: View { .foregroundColor(.w_greenBackground) .font(.system(size: 17.0, weight: .bold)) .clipShape(Circle()) - + Text("Chat Requests") .foregroundColor(.w_greenForground) .font(.system(size: 17.0, weight: .bold)) @@ -32,8 +32,8 @@ struct ChatListView: View { .clipShape(Capsule()) .padding(16.0) } - - if presenter.threads.isEmpty { + + if presenter.threadViewModels.isEmpty { Spacer() emptyView(size: geometry.size) Spacer() @@ -43,20 +43,19 @@ struct ChatListView: View { } } - Button("Log out") { - presenter.didLogoutPress() + PlainButton { + try await presenter.didLogoutPress() + } label: { + Text("Log out") + .foregroundColor(.red) } - .foregroundColor(.red) .padding(.bottom, 16) } - .onAppear { - presenter.setupInitialState() - } } } private func chatsList() -> some View { - ForEach(presenter.threads) { thread in + ForEach(presenter.threadViewModels) { thread in Button(action: { presenter.didPressThread(thread) }) { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift index 51403f1cb..1c37f30d8 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift @@ -13,7 +13,7 @@ struct ThreadViewModel: Identifiable { } var title: String { - return AccountNameResolver.resolveName(thread.peerAccount) + return thread.peerAccount.address } var subtitle: String { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift index 81d70a023..95c1cf133 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift @@ -1,17 +1,18 @@ final class ImportInteractor { - private let registerService: RegisterService + + private let chatService: ChatService private let accountStorage: AccountStorage - init(registerService: RegisterService, accountStorage: AccountStorage) { - self.registerService = registerService + init(chatService: ChatService, accountStorage: AccountStorage) { + self.chatService = chatService self.accountStorage = accountStorage } - func save(account: Account) { - accountStorage.account = account + func save(importAccount: ImportAccount) { + accountStorage.importAccount = importAccount } - func register(account: Account) async { - await registerService.register(account: account) + func register(importAccount: ImportAccount) async throws { + try await chatService.register(account: importAccount.account, privateKey: importAccount.privateKey) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift index 621af039b..4a567ca90 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift @@ -5,7 +5,7 @@ final class ImportModule { @discardableResult static func create(app: Application) -> UIViewController { let router = ImportRouter(app: app) - let interactor = ImportInteractor(registerService: app.registerService, accountStorage: app.accountStorage) + let interactor = ImportInteractor(chatService: app.chatService, accountStorage: app.accountStorage) let presenter = ImportPresenter(interactor: interactor, router: router) let view = ImportView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index 09428a950..114130e41 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -16,12 +16,13 @@ final class ImportPresenter: ObservableObject { } @MainActor - func didPressImport() async { - guard let account = AccountNameResolver.resolveAccount(input) + func didPressImport() async throws { + guard let importAccount = ImportAccount(input: input) else { return input = .empty } - interactor.save(account: account) - await interactor.register(account: account) - router.presentChat(account: account) + + interactor.save(importAccount: importAccount) + try await interactor.register(importAccount: importAccount) + router.presentChat(importAccount: importAccount) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift index 9b8b2ac8f..503509d36 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift @@ -10,7 +10,7 @@ final class ImportRouter { self.app = app } - func presentChat(account: Account) { - MainModule.create(app: app, account: account).present() + func presentChat(importAccount: ImportAccount) { + MainModule.create(app: app, importAccount: importAccount).present() } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift index 028156aaf..969c66834 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift @@ -11,13 +11,13 @@ struct ImportView: View { .frame(width: 128, height: 128) .padding(.top, 24.0) - TextFieldView(title: "Username", placeholder: "username.eth or 0x0…", input: $presenter.input) + TextFieldView(title: "Private key", placeholder: "4dc0055d1831…", input: $presenter.input) Spacer() - BrandButton(title: "Ok, done" ) { Task(priority: .userInitiated) { - await presenter.didPressImport() - }} + BrandButton(title: "Ok, done" ) { + try await presenter.didPressImport() + } .padding(16.0) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift index e707b2a1a..309251dc6 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift @@ -1,11 +1,14 @@ final class InviteInteractor { + + private let accountStorage: AccountStorage private let chatService: ChatService - init(chatService: ChatService) { + init(accountStorage: AccountStorage, chatService: ChatService) { + self.accountStorage = accountStorage self.chatService = chatService } - func invite(peerAccount: Account, message: String, selfAccount: Account) async { - try! await chatService.invite(peerAccount: peerAccount, message: message) + func invite(inviterAccount: Account, inviteeAccount: Account, message: String) async throws { + try await chatService.invite(inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, message: message) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift index 8a9d0e161..a114a92c3 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift @@ -5,7 +5,7 @@ final class InviteModule { @discardableResult static func create(app: Application, account: Account) -> UIViewController { let router = InviteRouter(app: app) - let interactor = InviteInteractor(chatService: app.chatService) + let interactor = InviteInteractor(accountStorage: app.accountStorage, chatService: app.chatService) let presenter = InvitePresenter(interactor: interactor, router: router, account: account) let view = InviteView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift index f1a320a48..54374f459 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import Web3 final class InvitePresenter: ObservableObject { @@ -12,16 +13,9 @@ final class InvitePresenter: ObservableObject { didSet { didInputChanged() } } - lazy var rightBarButtonItem: UIBarButtonItem? = { - let item = UIBarButtonItem( - title: "Invite", - style: .plain, - target: self, - action: #selector(invite) - ) - item.isEnabled = false - return item - }() + var showButton: Bool { + return resolveAccount(from: input) != nil + } init(interactor: InviteInteractor, router: InviteRouter, account: Account) { self.interactor = interactor @@ -30,8 +24,12 @@ final class InvitePresenter: ObservableObject { } @MainActor - func setupInitialState() async { + func invite() async throws { + guard let inviteeAccount = resolveAccount(from: input) + else { return } + try await interactor.invite(inviterAccount: account, inviteeAccount: inviteeAccount, message: "Welcome to WalletConnect Chat!") + router.dismiss() } } @@ -52,16 +50,20 @@ extension InvitePresenter: SceneViewModel { private extension InvitePresenter { - @MainActor - @objc func invite() { - guard let peerAccount = AccountNameResolver.resolveAccount(input) else { return } - Task(priority: .userInitiated) { - await interactor.invite(peerAccount: peerAccount, message: "Welcome to WalletConnect Chat!", selfAccount: account) - router.dismiss() - } - } - func didInputChanged() { rightBarButtonItem?.isEnabled = !input.isEmpty } + + func resolveAccount(from input: String) -> Account? { + if let account = Account(input) { + return account + } + if let account = ImportAccount(input: input)?.account { + return account + } + if let address = try? EthereumAddress(hex: input, eip55: false) { + return Account("eip155:1:\(address.hex(eip55: true))")! + } + return nil + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift index ccf6077ca..178faed50 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift @@ -5,11 +5,31 @@ struct InviteView: View { @EnvironmentObject var presenter: InvitePresenter var body: some View { - VStack { + VStack(spacing: 32) { TextFieldView(title: "ENS Name or Public Key", placeholder: "username.eth or 0x0…", input: $presenter.input) + + if presenter.showButton { + PlainButton { + try await presenter.invite() + } label: { + HStack(spacing: 8.0) { + Image("plus_icon") + .resizable() + .frame(width: 24, height: 24) + Text("Invite") + .foregroundColor(.w_foreground) + .font(.system(size: 18, weight: .semibold)) + } + .padding(.trailing, 8.0) + } + .frame(width: 128, height: 44) + .background( + Capsule() + .foregroundColor(.w_greenForground) + ) + } + Spacer() - }.task { - await presenter.setupInitialState() } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift index 1880de742..59ee20151 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift @@ -7,19 +7,19 @@ final class InviteListInteractor { self.chatService = chatService } - func getInvites() -> [Invite] { - return chatService.getInvites() + func getReceivedInvites() -> [ReceivedInvite] { + return chatService.getReceivedInvites() } - func invitesSubscription() -> Stream { - return chatService.invitePublisher + func invitesReceivedSubscription() -> Stream<[ReceivedInvite]> { + return chatService.receivedInvitePublisher } - func accept(invite: Invite) async { - try! await chatService.accept(invite: invite) + func accept(invite: ReceivedInvite) async throws { + try await chatService.accept(invite: invite) } - func reject(invite: Invite) async { - try! await chatService.reject(invite: invite) + func reject(invite: ReceivedInvite) async throws { + try await chatService.reject(invite: invite) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift index 915f08ed4..af3eaa08b 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift @@ -9,35 +9,27 @@ final class InviteListPresenter: ObservableObject { private let account: Account private var disposeBag = Set() - @Published var invites: [InviteViewModel] = [] + @Published private var receivedInvites: [ReceivedInvite] = [] + + var invites: [InviteViewModel] { + return receivedInvites + .sorted(by: { $0.timestamp > $1.timestamp }) + .map { InviteViewModel(invite: $0) } + } init(interactor: InviteListInteractor, router: InviteListRouter, account: Account) { + defer { setupInitialState() } self.interactor = interactor self.router = router self.account = account } - @MainActor - func setupInitialState() async { - loadInvites() - - for await _ in interactor.invitesSubscription() { - loadInvites() - } + func didPressAccept(invite: InviteViewModel) async throws { + try await interactor.accept(invite: invite.invite) } - func didPressAccept(invite: InviteViewModel) { - Task(priority: .userInitiated) { - await interactor.accept(invite: invite.invite) - await dismiss() - } - } - - func didPressReject(invite: InviteViewModel) { - Task(priority: .userInitiated) { - await interactor.reject(invite: invite.invite) - await dismiss() - } + func didPressReject(invite: InviteViewModel) async throws { + try await interactor.reject(invite: invite.invite) } } @@ -58,10 +50,13 @@ extension InviteListPresenter: SceneViewModel { private extension InviteListPresenter { - func loadInvites() { - invites = interactor.getInvites() - .sorted(by: { $0.publicKey < $1.publicKey }) - .map { InviteViewModel(invite: $0) } + func setupInitialState() { + receivedInvites = interactor.getReceivedInvites() + + interactor.invitesReceivedSubscription() + .sink { [unowned self] receivedInvites in + self.receivedInvites = receivedInvites + }.store(in: &disposeBag) } @MainActor diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift index 1a14aa497..e4adb21a3 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift @@ -10,7 +10,7 @@ struct InviteListView: View { Spacer() .frame(height: 16.0) - ForEach(presenter.invites, id: \.subtitle) { invite in + ForEach(presenter.invites) { invite in HStack(spacing: 16.0) { Image("avatar") .resizable() @@ -30,36 +30,44 @@ struct InviteListView: View { Spacer() - HStack(spacing: 8.0) { - Button(action: { presenter.didPressAccept(invite: invite) }) { - Image("checkmark_icon") - .resizable() - .frame(width: 32, height: 32) - } - Button(action: { presenter.didPressReject(invite: invite) }) { - Image("cross_icon") - .resizable() - .frame(width: 32, height: 32) + if invite.showActions { + HStack(spacing: 8.0) { + PlainButton { + try await presenter.didPressAccept(invite: invite) + } label: { + Image("checkmark_icon") + .resizable() + .frame(width: 32, height: 32) + } + + PlainButton { + try await presenter.didPressReject(invite: invite) + } label: { + Image("cross_icon") + .resizable() + .frame(width: 32, height: 32) + } } + .padding(4.0) + .background( + Capsule() + .foregroundColor(.w_secondaryBackground) + ) + .overlay( + Capsule() + .stroke(Color.w_tertiaryBackground, lineWidth: 0.5) + ) + } else { + Text(invite.statusTitle) + .font(.subheadline) + .foregroundColor(.w_secondaryForeground) } - .padding(4.0) - .background( - Capsule() - .foregroundColor(.w_secondaryBackground) - ) - .overlay( - Capsule() - .stroke(Color.w_tertiaryBackground, lineWidth: 0.5) - ) } .frame(height: 64.0) } .padding(16.0) } } - .task { - await presenter.setupInitialState() - } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift index 1dcc4a447..1f290a33d 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift @@ -1,18 +1,37 @@ import Foundation import WalletConnectChat -struct InviteViewModel { - let invite: Invite +struct InviteViewModel: Identifiable { + let invite: ReceivedInvite - init(invite: Invite) { + var id: Int64 { + return invite.id + } + + init(invite: ReceivedInvite) { self.invite = invite } var title: String { - return AccountNameResolver.resolveName(invite.account) + return invite.inviterAccount.address } var subtitle: String { return invite.message } + + var showActions: Bool { + return invite.status == .pending + } + + var statusTitle: String { + switch invite.status { + case .pending: + return "Pending" + case .approved: + return "Approved" + case .rejected: + return "Rejected" + } + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift index fd6309823..2139862bf 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift @@ -3,9 +3,9 @@ import SwiftUI final class MainModule { @discardableResult - static func create(app: Application, account: Account) -> UIViewController { + static func create(app: Application, importAccount: ImportAccount) -> UIViewController { let router = MainRouter(app: app) - let presenter = MainPresenter(router: router, account: account) + let presenter = MainPresenter(router: router, importAccount: importAccount) let viewController = MainViewController(presenter: presenter) router.viewController = viewController diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift index 10d1de36f..fc52791a8 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift @@ -3,7 +3,7 @@ import Combine final class MainPresenter { - private let account: Account + private let importAccount: ImportAccount private let router: MainRouter var tabs: [TabPage] { @@ -12,13 +12,13 @@ final class MainPresenter { var viewControllers: [UIViewController] { return [ - router.chatViewController(account: account), - router.web3InboxViewController(account: account), + router.chatViewController(account: importAccount.account), + router.web3InboxViewController(importAccount: importAccount), ] } - init(router: MainRouter, account: Account) { - self.account = account + init(router: MainRouter, importAccount: ImportAccount) { + self.importAccount = importAccount self.router = router } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift index 9072fcdf1..e0fc6d0e6 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift @@ -10,8 +10,8 @@ final class MainRouter { return ChatListModule.create(app: app, account: account).wrapToNavigationController() } - func web3InboxViewController(account: Account) -> UIViewController { - return Web3InboxModule.create(app: app, account: account).wrapToNavigationController() + func web3InboxViewController(importAccount: ImportAccount) -> UIViewController { + return Web3InboxModule.create(app: app, importAccount: importAccount).wrapToNavigationController() } init(app: Application) { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift index 548e47f5e..318dce292 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift @@ -16,12 +16,12 @@ final class WelcomeInteractor { self.accountStorage = accountStorage } - var account: Account? { - return accountStorage.account + var importAccount: ImportAccount? { + return accountStorage.importAccount } func isAuthorized() -> Bool { - accountStorage.account != nil + accountStorage.importAccount != nil } func trackConnection() -> Stream { @@ -31,6 +31,11 @@ final class WelcomeInteractor { func generateUri() async -> WalletConnectURI { return try! await Pair.instance.create() } + + func goPublic() async throws { + guard let importAccount = importAccount else { return } + try await chatService.goPublic(account: importAccount.account, privateKey: importAccount.privateKey) + } } protocol IATProvider { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift index dd9d00f1f..db4e676f4 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift @@ -7,30 +7,32 @@ final class WelcomePresenter: ObservableObject { private let router: WelcomeRouter private let interactor: WelcomeInteractor - @Published var connected: Bool = false + private var disposeBag = Set() init(router: WelcomeRouter, interactor: WelcomeInteractor) { + defer { setupInitialState() } self.router = router self.interactor = interactor } - @MainActor - func setupInitialState() async { - for await connected in interactor.trackConnection() { - print("Client connection status: \(connected)") - self.connected = connected == .connected - } - } - var buttonTitle: String { return interactor.isAuthorized() ? "Start Messaging" : "Connect wallet" } - func didPressImport() { - if let account = interactor.account { - router.presentMain(account: account) + @MainActor + func didPressImport() async throws { + if let importAccount = interactor.importAccount { + try await interactor.goPublic() + router.presentMain(importAccount: importAccount) } else { router.presentImport() } } } + +private extension WelcomePresenter { + + func setupInitialState() { + + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift index b6ff4209d..da574fb23 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift @@ -16,8 +16,8 @@ final class WelcomeRouter { .present() } - func presentMain(account: Account) { - MainModule.create(app: app, account: account) + func presentMain(importAccount: ImportAccount) { + MainModule.create(app: app, importAccount: importAccount) .present() } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift index 2d9ba4129..05609b612 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift @@ -31,9 +31,9 @@ struct WelcomeView: View { .foregroundColor(.w_foreground) .multilineTextAlignment(.center) - BrandButton(title: presenter.buttonTitle, action: { - presenter.didPressImport() - }) + BrandButton(title: presenter.buttonTitle) { + try await presenter.didPressImport() + } Text("By connecting your wallet you agree with our\nTerms of Service") .font(.footnote) @@ -48,9 +48,6 @@ struct WelcomeView: View { offset = -(UIScreen.main.bounds.height / 4) } } - .task { - await presenter.setupInitialState() - } } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxModule.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxModule.swift index fe2242603..9b96061fd 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxModule.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxModule.swift @@ -3,9 +3,9 @@ import SwiftUI final class Web3InboxModule { @discardableResult - static func create(app: Application, account: Account) -> UIViewController { + static func create(app: Application, importAccount: ImportAccount) -> UIViewController { let router = Web3InboxRouter(app: app) - let viewController = Web3InboxViewController(account: account) + let viewController = Web3InboxViewController(importAccount: importAccount) router.viewController = viewController return viewController } diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index 7f5d62ab3..d1dba61b7 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -1,13 +1,13 @@ import UIKit -import Web3Inbox import WebKit +import Web3Inbox final class Web3InboxViewController: UIViewController { - private let account: Account + private let importAccount: ImportAccount - init(account: Account) { - self.account = account + init(importAccount: ImportAccount) { + self.importAccount = importAccount super.init(nibName: nil, bundle: nil) } @@ -18,9 +18,19 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: account) + Web3Inbox.configure(account: importAccount.account, onSign: onSing) view = Web3Inbox.instance.getWebView() navigationItem.title = "Web3Inbox SDK" } } + +private extension Web3InboxViewController { + + func onSing(_ message: String) -> SigningResult { + let privateKey = Data(hex: importAccount.privateKey) + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() + let signature = try! signer.sign(message: message, privateKey: privateKey, type: .eip191) + return .signed(signature) + } +} diff --git a/Example/Showcase/Common/Components/BrandButton.swift b/Example/Showcase/Common/Components/BrandButton.swift index 85c02f614..6fee139db 100644 --- a/Example/Showcase/Common/Components/BrandButton.swift +++ b/Example/Showcase/Common/Components/BrandButton.swift @@ -1,20 +1,25 @@ import SwiftUI +import AsyncButton struct BrandButton: View { let title: String - let action: () -> Void + let action: () async throws -> Void var body: some View { - Button(action: { action() }, label: { + AsyncButton(options: [.automatic]) { + try await action() + } label: { Text(title) .foregroundColor(.w_foreground) .font(.system(size: 20, weight: .bold)) - }) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background( - Capsule() - .foregroundColor(.w_greenForground) - ) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background( + Capsule() + .foregroundColor(.w_greenForground) + ) + } } } + + diff --git a/Example/Showcase/Common/Components/PlainButton.swift b/Example/Showcase/Common/Components/PlainButton.swift new file mode 100644 index 000000000..1baa3cb9c --- /dev/null +++ b/Example/Showcase/Common/Components/PlainButton.swift @@ -0,0 +1,16 @@ +import SwiftUI +import AsyncButton + +struct PlainButton