diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml index f8d0a3743..a9639e3db 100644 --- a/.github/actions/run_tests_without_building/action.yml +++ b/.github/actions/run_tests_without_building/action.yml @@ -24,6 +24,10 @@ inputs: gm-dapp-project-secret: description: 'GM DApp Project Secret' required: false + gm-dapp-host: + description: 'GM DApp Host' + required: false + default: 'gm.walletconnect.com' js-client-api-host: description: 'JS Client Api Host' required: false @@ -55,7 +59,7 @@ runs: - name: Run integration tests if: inputs.type == 'integration-tests' shell: bash - run: make integration_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} JS_CLIENT_API_HOST=${{ inputs.js-client-api-host }} + run: make integration_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} GM_DAPP_HOST=${{ inputs.gm-dapp-host }} JS_CLIENT_API_HOST=${{ inputs.js-client-api-host }} # Relay Integration tests - name: Run Relay integration tests @@ -73,7 +77,7 @@ runs: - name: Run notify tests if: inputs.type == 'notify-tests' shell: bash - run: make notify_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} + run: make notify_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} GM_DAPP_HOST=${{ inputs.gm-dapp-host }} - name: Run x-platform protocol tests if: inputs.type == 'x-platform-protocol-tests' diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 500646255..1f26d47f9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,7 +13,8 @@ env: PACKAGE_VERSION: ${{ github.event.pull_request.title }} jobs: set-user-agent: - runs-on: macos-latest + runs-on: + group: apple-silicon steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef5f81953..5a9b07721 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: - name: Run integration tests if: matrix.type == 'integration-tests' shell: bash - run: make integration_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} CAST_HOST=notify.walletconnect.com GM_DAPP_PROJECT_ID=${{ secrets.GM_DAPP_PROJECT_ID }} GM_DAPP_PROJECT_SECRET=${{ secrets.GM_DAPP_PROJECT_SECRET }} JS_CLIENT_API_HOST=test-automation-api.walletconnect.com + run: make integration_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} CAST_HOST=notify.walletconnect.com GM_DAPP_PROJECT_ID=${{ secrets.GM_DAPP_PROJECT_ID }} GM_DAPP_PROJECT_SECRET=${{ secrets.GM_DAPP_PROJECT_SECRET }} GM_DAPP_HOST=gm.walletconnect.com JS_CLIENT_API_HOST=test-automation-api.walletconnect.com # Relay Integration tests - name: Run Relay integration tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3174bdc93..df62cc8a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,8 @@ on: jobs: build: - runs-on: macos-12 + runs-on: + group: apple-silicon steps: - uses: actions/checkout@v3 @@ -35,4 +36,4 @@ jobs: APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} WALLETAPP_SENTRY_DSN: ${{ secrets.WALLETAPP_SENTRY_DSN }} run: | - make release_all APPLE_ID=${{ secrets.APPLE_ID }} TOKEN=$(echo -n $GH_USER:$GH_TOKEN | base64) PROJECT_ID=${{ secrets.RELEASE_PROJECT_ID }} WALLETAPP_SENTRY_DSN=${{ secrets.WALLETAPP_SENTRY_DSN }} MIXPANEL_TOKEN=${{secrets.MIXPANEL_TOKEN}} + make release_wallet APPLE_ID=${{ secrets.APPLE_ID }} TOKEN=$(echo -n $GH_USER:$GH_TOKEN | base64) PROJECT_ID=${{ secrets.RELEASE_PROJECT_ID }} WALLETAPP_SENTRY_DSN=${{ secrets.WALLETAPP_SENTRY_DSN }} MIXPANEL_TOKEN=${{secrets.MIXPANEL_TOKEN}} diff --git a/Configuration.xcconfig b/Configuration.xcconfig index 361714e46..98daaddd4 100644 --- a/Configuration.xcconfig +++ b/Configuration.xcconfig @@ -9,6 +9,9 @@ RELAY_HOST = relay.walletconnect.com // Uncomment next line and paste dapp's project secret to run all the notify tests // GM_DAPP_PROJECT_SECRET = GM_DAPP_PROJECT_SECRET +// Uncomment next line and paste dapp's host to run all the notify tests +// GM_DAPP_HOST = GM_DAPP_HOST + // Uncomment next line and paste js client's api host to run x-platform tests // JS_CLIENT_API_HOST = JS_CLIENT_API_HOST diff --git a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan index 0fb2db965..e3aa93b52 100644 --- a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan +++ b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan @@ -15,6 +15,10 @@ "key" : "RELAY_HOST", "value" : "$(RELAY_HOST)" }, + { + "key" : "GM_DAPP_HOST", + "value" : "$(GM_DAPP_HOST)" + }, { "key" : "JS_CLIENT_API_HOST", "value" : "$(JS_CLIENT_API_HOST)" @@ -48,7 +52,10 @@ "skippedTests" : [ "AuthTests\/testEIP1271RespondSuccess()", "ChatTests", - "ENSResolverTests" + "ENSResolverTests", + "HistoryTests", + "SyncDerivationServiceTests", + "SyncTests" ], "target" : { "containerPath" : "container:ExampleApp.xcodeproj", diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 14e537032..27c7d60a4 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -16,10 +16,6 @@ 844749FE29B9EB1B005F520B /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518B31328E33A6500A2CE93 /* InputConfig.swift */; }; 844749FF29B9EB3B005F520B /* KeychainStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E03E1028646F8000888481 /* KeychainStorageMock.swift */; }; 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 8448F1D327E4726F0000B866 /* WalletConnect */; }; - 84536D6E29EEAE1F008EA8DB /* Web3InboxModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84536D6D29EEAE1F008EA8DB /* Web3InboxModule.swift */; }; - 84536D7029EEAE28008EA8DB /* Web3InboxRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84536D6F29EEAE28008EA8DB /* Web3InboxRouter.swift */; }; - 84536D7229EEAE32008EA8DB /* Web3InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84536D7129EEAE32008EA8DB /* Web3InboxViewController.swift */; }; - 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */ = {isa = PBXBuildFile; productRef = 84536D7329EEBCF0008EA8DB /* Web3Inbox */; }; 845B8D8C2934B36C0084A966 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B8D8B2934B36C0084A966 /* Account.swift */; }; 847BD1D62989492500076C90 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1D12989492500076C90 /* MainViewController.swift */; }; 847BD1D82989492500076C90 /* MainModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1D32989492500076C90 /* MainModule.swift */; }; @@ -43,12 +39,7 @@ 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A6E3C22A386BBC008A0571 /* Publisher.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154D2991099000FAD54E /* BuildConfiguration.swift */; }; - 84B815542991217900FAD54E /* PushMessagesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154F2991217900FAD54E /* PushMessagesModule.swift */; }; - 84B815552991217900FAD54E /* PushMessagesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B815502991217900FAD54E /* PushMessagesPresenter.swift */; }; - 84B815562991217900FAD54E /* PushMessagesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B815512991217900FAD54E /* PushMessagesRouter.swift */; }; - 84B815572991217900FAD54E /* PushMessagesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B815522991217900FAD54E /* PushMessagesInteractor.swift */; }; - 84B815582991217900FAD54E /* PushMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B815532991217900FAD54E /* PushMessagesView.swift */; }; - 84B8155B2992A18D00FAD54E /* PushMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8155A2992A18D00FAD54E /* PushMessageViewModel.swift */; }; + 84B8155B2992A18D00FAD54E /* NotifyMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */; }; 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE641E27981DED00142511 /* AppDelegate.swift */; }; 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE642027981DED00142511 /* SceneDelegate.swift */; }; 84CE642827981DF000142511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642727981DF000142511 /* Assets.xcassets */; }; @@ -71,6 +62,11 @@ 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507BE1929E8032E0038EF70 /* EIP55Tests.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; + A50D53C12ABA055700A4FD8B /* NotifyPreferencesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BC2ABA055700A4FD8B /* NotifyPreferencesModule.swift */; }; + A50D53C22ABA055700A4FD8B /* NotifyPreferencesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BD2ABA055700A4FD8B /* NotifyPreferencesPresenter.swift */; }; + A50D53C32ABA055700A4FD8B /* NotifyPreferencesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BE2ABA055700A4FD8B /* NotifyPreferencesRouter.swift */; }; + A50D53C42ABA055700A4FD8B /* NotifyPreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BF2ABA055700A4FD8B /* NotifyPreferencesInteractor.swift */; }; + A50D53C52ABA055700A4FD8B /* NotifyPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53C02ABA055700A4FD8B /* NotifyPreferencesView.swift */; }; A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */ = {isa = PBXBuildFile; productRef = A50DF19C2A25084A0036EA6C /* WalletConnectHistory */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; @@ -118,6 +114,8 @@ A5629AE42876E6D200094373 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AE32876E6D200094373 /* ThreadViewModel.swift */; }; 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 */; }; + A56AC8F22AD88A5A001C8FAA /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56AC8F12AD88A5A001C8FAA /* Sequence.swift */; }; A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */; }; A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53829EC365000E3CBFD /* HDWalletKit */; }; A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53A29EC365800E3CBFD /* HDWalletKit */; }; @@ -161,6 +159,7 @@ 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 */; }; + A59D25EE2AB3672700D7EA3A /* AsyncButton in Frameworks */ = {isa = PBXBuildFile; productRef = A59D25ED2AB3672700D7EA3A /* AsyncButton */; }; A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A59F877528B5462900A9CD80 /* WalletConnectAuth */; }; A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A59FAEC828B7B93A002BB66F /* Web3 */; }; A5A0843D29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A0843B29D2F60A000B9B17 /* DefaultCryptoProvider.swift */; }; @@ -170,6 +169,12 @@ A5A4FC772840C12C00BBEC1E /* RegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */; }; A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; A5A8E480293A1D0000FEB97D /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; + A5B4F7C22ABB20AE0099AF7C /* SubscriptionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7BD2ABB20AE0099AF7C /* SubscriptionPresenter.swift */; }; + A5B4F7C32ABB20AE0099AF7C /* SubscriptionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7BE2ABB20AE0099AF7C /* SubscriptionInteractor.swift */; }; + A5B4F7C42ABB20AE0099AF7C /* SubscriptionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7BF2ABB20AE0099AF7C /* SubscriptionModule.swift */; }; + A5B4F7C52ABB20AE0099AF7C /* SubscriptionRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7C02ABB20AE0099AF7C /* SubscriptionRouter.swift */; }; + A5B4F7C62ABB20AE0099AF7C /* SubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7C12ABB20AE0099AF7C /* SubscriptionView.swift */; }; + A5B4F7C82ABB21190099AF7C /* CacheAsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4F7C72ABB21190099AF7C /* CacheAsyncImage.swift */; }; A5B6C0F12A6EAB0800927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F02A6EAB0800927332 /* WalletConnectNotify */; }; A5B6C0F32A6EAB1700927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */; }; A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */; }; @@ -195,6 +200,14 @@ A5C2022B287EB89A007E3188 /* WelcomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C2022A287EB89A007E3188 /* WelcomeInteractor.swift */; }; A5C5153329BB7A6A004210BA /* InviteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C5153229BB7A6A004210BA /* InviteType.swift */; }; A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A5C8BE84292FE20B006CC85C /* Web3 */; }; + A5D610C82AB31EE800C20083 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610C72AB31EE800C20083 /* SegmentedPicker.swift */; }; + A5D610CA2AB3249100C20083 /* ListingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610C92AB3249100C20083 /* ListingViewModel.swift */; }; + A5D610CE2AB3594100C20083 /* ListingsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610CD2AB3594100C20083 /* ListingsAPI.swift */; }; + A5D610D02AB35AD500C20083 /* ListingsNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610CF2AB35AD500C20083 /* ListingsNetworkService.swift */; }; + A5D610D22AB35B1100C20083 /* Listings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610D12AB35B1100C20083 /* Listings.swift */; }; + A5D610D42AB35BED00C20083 /* FailableDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D610D32AB35BED00C20083 /* FailableDecodable.swift */; }; + A5D85228286333E300DAF5C3 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5D85227286333E300DAF5C3 /* Starscream */; }; + A5E03DF52864651200888481 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5E03DF42864651200888481 /* Starscream */; }; A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E03DF9286465C700888481 /* SignClientTests.swift */; }; A5E03DFD286465D100888481 /* Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E03DFC286465D100888481 /* Stubs.swift */; }; A5E03DFF2864662500888481 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = A5E03DFE2864662500888481 /* WalletConnect */; }; @@ -209,6 +222,7 @@ A5E22D242840C8DB00E36487 /* SafariEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D232840C8DB00E36487 /* SafariEngine.swift */; }; A5E22D2C2840EAC300E36487 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D2B2840EAC300E36487 /* XCUIElement.swift */; }; A5E776BA29F4362D00172091 /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E776B929F4362D00172091 /* AlertError.swift */; }; + A5F1526F2ACDC46B00D745A6 /* Web3ModalUI in Frameworks */ = {isa = PBXBuildFile; productRef = A5F1526E2ACDC46B00D745A6 /* Web3ModalUI */; }; A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */; }; C5133A78294125CC00A8314C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = C5133A77294125CC00A8314C /* Web3 */; }; C53AA4362941251C008EA57C /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; @@ -294,6 +308,13 @@ C5F32A322954816C00A6476E /* ConnectionDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A312954816C00A6476E /* ConnectionDetailsPresenter.swift */; }; C5F32A342954817600A6476E /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A332954817600A6476E /* ConnectionDetailsView.swift */; }; C5F32A362954FE3C00A6476E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5F32A352954FE3C00A6476E /* Colors.xcassets */; }; + C5FFEA762ADD8956007282A2 /* BrowserModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA752ADD8956007282A2 /* BrowserModule.swift */; }; + C5FFEA782ADD896E007282A2 /* BrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA772ADD896E007282A2 /* BrowserPresenter.swift */; }; + C5FFEA7A2ADD8974007282A2 /* BrowserRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA792ADD8974007282A2 /* BrowserRouter.swift */; }; + C5FFEA7C2ADD897C007282A2 /* BrowserInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA7B2ADD897C007282A2 /* BrowserInteractor.swift */; }; + C5FFEA7E2ADD8985007282A2 /* BrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA7D2ADD8985007282A2 /* BrowserView.swift */; }; + C5FFEA812ADDACD7007282A2 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA802ADDACD7007282A2 /* WebView.swift */; }; + C5FFEA842ADDAD6D007282A2 /* SafariViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFEA832ADDAD6D007282A2 /* SafariViewController.swift */; }; CF1A594529E5876600AAC16B /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593A29E5876600AAC16B /* XCUIElement.swift */; }; CF1A594629E5876600AAC16B /* PushNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593C29E5876600AAC16B /* PushNotificationTests.swift */; }; CF1A594829E5876600AAC16B /* Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593F29E5876600AAC16B /* Engine.swift */; }; @@ -371,9 +392,6 @@ 8439CB88293F658E00F2F2E2 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = ""; }; 844749F329B9E5B9005F520B /* RelayIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RelayIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 844749F529B9E5B9005F520B /* RelayClientEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayClientEndToEndTests.swift; sourceTree = ""; }; - 84536D6D29EEAE1F008EA8DB /* Web3InboxModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3InboxModule.swift; sourceTree = ""; }; - 84536D6F29EEAE28008EA8DB /* Web3InboxRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3InboxRouter.swift; sourceTree = ""; }; - 84536D7129EEAE32008EA8DB /* Web3InboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3InboxViewController.swift; sourceTree = ""; }; 845AA7D929BA1EBA00F33739 /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = IntegrationTests.xctestplan; path = ExampleApp.xcodeproj/IntegrationTests.xctestplan; sourceTree = ""; }; 845AA7DC29BB424800F33739 /* SmokeTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SmokeTests.xctestplan; sourceTree = ""; }; 845B8D8B2934B36C0084A966 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; @@ -398,12 +416,7 @@ 84A6E3C22A386BBC008A0571 /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; 84B8154D2991099000FAD54E /* BuildConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; - 84B8154F2991217900FAD54E /* PushMessagesModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesModule.swift; sourceTree = ""; }; - 84B815502991217900FAD54E /* PushMessagesPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesPresenter.swift; sourceTree = ""; }; - 84B815512991217900FAD54E /* PushMessagesRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesRouter.swift; sourceTree = ""; }; - 84B815522991217900FAD54E /* PushMessagesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesInteractor.swift; sourceTree = ""; }; - 84B815532991217900FAD54E /* PushMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesView.swift; sourceTree = ""; }; - 84B8155A2992A18D00FAD54E /* PushMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessageViewModel.swift; sourceTree = ""; }; + 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyMessageViewModel.swift; sourceTree = ""; }; 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 84CE641E27981DED00142511 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 84CE642027981DED00142511 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -431,6 +444,11 @@ 84FE684528ACDB4700C893FF /* RequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestParams.swift; sourceTree = ""; }; A507BE1929E8032E0038EF70 /* EIP55Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EIP55Tests.swift; sourceTree = ""; }; A50C036428AAD32200FE72D3 /* ClientDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; + A50D53BC2ABA055700A4FD8B /* NotifyPreferencesModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPreferencesModule.swift; sourceTree = ""; }; + A50D53BD2ABA055700A4FD8B /* NotifyPreferencesPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPreferencesPresenter.swift; sourceTree = ""; }; + A50D53BE2ABA055700A4FD8B /* NotifyPreferencesRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPreferencesRouter.swift; sourceTree = ""; }; + A50D53BF2ABA055700A4FD8B /* NotifyPreferencesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPreferencesInteractor.swift; sourceTree = ""; }; + A50D53C02ABA055700A4FD8B /* NotifyPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPreferencesView.swift; sourceTree = ""; }; A50F3945288005B200064555 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBIP44Provider.swift; sourceTree = ""; }; A51811972A52E21A00A52B15 /* ConfigurationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationService.swift; sourceTree = ""; }; @@ -470,6 +488,8 @@ A5629ADD2876CC6E00094373 /* InviteListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteListView.swift; sourceTree = ""; }; A5629AE32876E6D200094373 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = ""; }; A5629AE728772A0100094373 /* InviteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteViewModel.swift; sourceTree = ""; }; + A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSocketFactory.swift; sourceTree = ""; }; + A56AC8F12AD88A5A001C8FAA /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = ""; }; A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDerivationServiceTests.swift; sourceTree = ""; }; A57879702A4EDC8100F8D10B /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; A578FA312873036400AA7720 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; @@ -513,6 +533,12 @@ A5A0843B29D2F60A000B9B17 /* DefaultCryptoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultCryptoProvider.swift; sourceTree = ""; }; A5A4FC722840C12C00BBEC1E /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegressionTests.swift; sourceTree = ""; }; + A5B4F7BD2ABB20AE0099AF7C /* SubscriptionPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionPresenter.swift; sourceTree = ""; }; + A5B4F7BE2ABB20AE0099AF7C /* SubscriptionInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionInteractor.swift; sourceTree = ""; }; + A5B4F7BF2ABB20AE0099AF7C /* SubscriptionModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionModule.swift; sourceTree = ""; }; + A5B4F7C02ABB20AE0099AF7C /* SubscriptionRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRouter.swift; sourceTree = ""; }; + A5B4F7C12ABB20AE0099AF7C /* SubscriptionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionView.swift; sourceTree = ""; }; + A5B4F7C72ABB21190099AF7C /* CacheAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheAsyncImage.swift; sourceTree = ""; }; A5BB7F9E28B69B7100707FC6 /* SignCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignCoordinator.swift; sourceTree = ""; }; A5BB7FA028B69F3400707FC6 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; A5BB7FA628B6A5F600707FC6 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; @@ -532,6 +558,12 @@ A5C20228287EB34C007E3188 /* AccountStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStorage.swift; sourceTree = ""; }; A5C2022A287EB89A007E3188 /* WelcomeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeInteractor.swift; sourceTree = ""; }; A5C5153229BB7A6A004210BA /* InviteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteType.swift; sourceTree = ""; }; + A5D610C72AB31EE800C20083 /* SegmentedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPicker.swift; sourceTree = ""; }; + A5D610C92AB3249100C20083 /* ListingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingViewModel.swift; sourceTree = ""; }; + A5D610CD2AB3594100C20083 /* ListingsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingsAPI.swift; sourceTree = ""; }; + A5D610CF2AB35AD500C20083 /* ListingsNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingsNetworkService.swift; sourceTree = ""; }; + A5D610D12AB35B1100C20083 /* Listings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Listings.swift; sourceTree = ""; }; + A5D610D32AB35BED00C20083 /* FailableDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailableDecodable.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 = ""; }; @@ -613,6 +645,13 @@ C5F32A312954816C00A6476E /* ConnectionDetailsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsPresenter.swift; sourceTree = ""; }; C5F32A332954817600A6476E /* ConnectionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionDetailsView.swift; sourceTree = ""; }; C5F32A352954FE3C00A6476E /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; + C5FFEA752ADD8956007282A2 /* BrowserModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserModule.swift; sourceTree = ""; }; + C5FFEA772ADD896E007282A2 /* BrowserPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserPresenter.swift; sourceTree = ""; }; + C5FFEA792ADD8974007282A2 /* BrowserRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserRouter.swift; sourceTree = ""; }; + C5FFEA7B2ADD897C007282A2 /* BrowserInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserInteractor.swift; sourceTree = ""; }; + C5FFEA7D2ADD8985007282A2 /* BrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserView.swift; sourceTree = ""; }; + C5FFEA802ADDACD7007282A2 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + C5FFEA832ADDAD6D007282A2 /* SafariViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariViewController.swift; sourceTree = ""; }; CF1A593029E5873D00AAC16B /* EchoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EchoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CF1A593A29E5876600AAC16B /* XCUIElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; CF1A593C29E5876600AAC16B /* PushNotificationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationTests.swift; sourceTree = ""; }; @@ -705,13 +744,14 @@ buildActionMask = 2147483647; files = ( A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */, + A59D25EE2AB3672700D7EA3A /* AsyncButton in Frameworks */, C56EE27D293F56F8004840D1 /* WalletConnectChat in Frameworks */, C5133A78294125CC00A8314C /* Web3 in Frameworks */, - C5CFBED12AA9A31500378D41 /* Starscream in Frameworks */, - 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */, C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */, 8487A9462A836C3F0003D5AF /* Sentry in Frameworks */, C55D349929630D440004314A /* Web3Wallet in Frameworks */, + A5F1526F2ACDC46B00D745A6 /* Web3ModalUI in Frameworks */, + C56EE255293F569A004840D1 /* Starscream in Frameworks */, A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */, C56EE27B293F56F8004840D1 /* WalletConnectAuth in Frameworks */, 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */, @@ -795,16 +835,6 @@ path = RelayIntegrationTests; sourceTree = ""; }; - 84536D6C29EEAE0D008EA8DB /* Web3Inbox */ = { - isa = PBXGroup; - children = ( - 84536D6D29EEAE1F008EA8DB /* Web3InboxModule.swift */, - 84536D6F29EEAE28008EA8DB /* Web3InboxRouter.swift */, - 84536D7129EEAE32008EA8DB /* Web3InboxViewController.swift */, - ); - path = Web3Inbox; - sourceTree = ""; - }; 847BD1DB2989493F00076C90 /* Main */ = { isa = PBXGroup; children = ( @@ -843,6 +873,7 @@ isa = PBXGroup; children = ( 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */, + A5D610C92AB3249100C20083 /* ListingViewModel.swift */, ); path = Models; sourceTree = ""; @@ -875,11 +906,11 @@ 84B815592991217F00FAD54E /* PushMessages */ = { isa = PBXGroup; children = ( - 84B8154F2991217900FAD54E /* PushMessagesModule.swift */, - 84B815502991217900FAD54E /* PushMessagesPresenter.swift */, - 84B815512991217900FAD54E /* PushMessagesRouter.swift */, - 84B815522991217900FAD54E /* PushMessagesInteractor.swift */, - 84B815532991217900FAD54E /* PushMessagesView.swift */, + A5B4F7BE2ABB20AE0099AF7C /* SubscriptionInteractor.swift */, + A5B4F7BF2ABB20AE0099AF7C /* SubscriptionModule.swift */, + A5B4F7BD2ABB20AE0099AF7C /* SubscriptionPresenter.swift */, + A5B4F7C02ABB20AE0099AF7C /* SubscriptionRouter.swift */, + A5B4F7C12ABB20AE0099AF7C /* SubscriptionView.swift */, 84B8155C2992A19200FAD54E /* Models */, ); path = PushMessages; @@ -888,7 +919,7 @@ 84B8155C2992A19200FAD54E /* Models */ = { isa = PBXGroup; children = ( - 84B8155A2992A18D00FAD54E /* PushMessageViewModel.swift */, + 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */, ); path = Models; sourceTree = ""; @@ -982,6 +1013,18 @@ path = PNDecryptionService; sourceTree = ""; }; + A50D53BB2ABA053600A4FD8B /* NotifySettings */ = { + isa = PBXGroup; + children = ( + A50D53BC2ABA055700A4FD8B /* NotifyPreferencesModule.swift */, + A50D53BD2ABA055700A4FD8B /* NotifyPreferencesPresenter.swift */, + A50D53BE2ABA055700A4FD8B /* NotifyPreferencesRouter.swift */, + A50D53BF2ABA055700A4FD8B /* NotifyPreferencesInteractor.swift */, + A50D53C02ABA055700A4FD8B /* NotifyPreferencesView.swift */, + ); + path = NotifySettings; + sourceTree = ""; + }; A50F3944288005A700064555 /* Types */ = { isa = PBXGroup; children = ( @@ -1103,6 +1146,14 @@ path = Chat; sourceTree = ""; }; + A56AC8F02AD88A4B001C8FAA /* Foundation */ = { + isa = PBXGroup; + children = ( + A56AC8F12AD88A5A001C8FAA /* Sequence.swift */, + ); + path = Foundation; + sourceTree = ""; + }; A574B3592964570000C2BB91 /* Web3Inbox */ = { isa = PBXGroup; children = ( @@ -1428,6 +1479,24 @@ path = Components; sourceTree = ""; }; + A5D610CB2AB358ED00C20083 /* BusinessLayer */ = { + isa = PBXGroup; + children = ( + A5D610CC2AB3592F00C20083 /* ListingsSertice */, + ); + path = BusinessLayer; + sourceTree = ""; + }; + A5D610CC2AB3592F00C20083 /* ListingsSertice */ = { + isa = PBXGroup; + children = ( + A5D610CD2AB3594100C20083 /* ListingsAPI.swift */, + A5D610CF2AB35AD500C20083 /* ListingsNetworkService.swift */, + A5D610D12AB35B1100C20083 /* Listings.swift */, + ); + path = ListingsSertice; + sourceTree = ""; + }; A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( @@ -1546,6 +1615,7 @@ C56EE25C293F56D6004840D1 /* Common */, C56EE27E293F5756004840D1 /* ApplicationLayer */, C56EE29E293F577B004840D1 /* PresentationLayer */, + A5D610CB2AB358ED00C20083 /* BusinessLayer */, C56EE2A0293F6B10004840D1 /* Other */, ); path = WalletApp; @@ -1554,8 +1624,9 @@ C56EE229293F5668004840D1 /* Wallet */ = { isa = PBXGroup; children = ( + C5FFEA742ADD8942007282A2 /* Browser */, + A50D53BB2ABA053600A4FD8B /* NotifySettings */, A51811992A52E82100A52B15 /* Settings */, - 84536D6C29EEAE0D008EA8DB /* Web3Inbox */, 847BD1DB2989493F00076C90 /* Main */, C55D3477295DD4AA0004314A /* Welcome */, C55D3474295DCB850004314A /* AuthRequest */, @@ -1622,6 +1693,7 @@ C56EE262293F56D6004840D1 /* Extensions */ = { isa = PBXGroup; children = ( + A56AC8F02AD88A4B001C8FAA /* Foundation */, 84F568C32795832A00D0A289 /* EthereumTransaction.swift */, C56EE26D293F56D6004840D1 /* SwiftUI */, C56EE269293F56D6004840D1 /* UIKit */, @@ -1713,8 +1785,11 @@ isa = PBXGroup; children = ( C56EE2A2293F6BAF004840D1 /* UIPasteboardWrapper.swift */, + A5B4F7C72ABB21190099AF7C /* CacheAsyncImage.swift */, C55D349A2965BC2F0004314A /* TagsView.swift */, A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */, + A5D610C72AB31EE800C20083 /* SegmentedPicker.swift */, + A5D610D32AB35BED00C20083 /* FailableDecodable.swift */, ); path = Helpers; sourceTree = ""; @@ -1743,6 +1818,36 @@ path = ConnectionDetails; sourceTree = ""; }; + C5FFEA742ADD8942007282A2 /* Browser */ = { + isa = PBXGroup; + children = ( + C5FFEA822ADDAD5B007282A2 /* SafariViewController */, + C5FFEA7F2ADDACCC007282A2 /* WebView */, + C5FFEA752ADD8956007282A2 /* BrowserModule.swift */, + C5FFEA772ADD896E007282A2 /* BrowserPresenter.swift */, + C5FFEA792ADD8974007282A2 /* BrowserRouter.swift */, + C5FFEA7B2ADD897C007282A2 /* BrowserInteractor.swift */, + C5FFEA7D2ADD8985007282A2 /* BrowserView.swift */, + ); + path = Browser; + sourceTree = ""; + }; + C5FFEA7F2ADDACCC007282A2 /* WebView */ = { + isa = PBXGroup; + children = ( + C5FFEA802ADDACD7007282A2 /* WebView.swift */, + ); + path = WebView; + sourceTree = ""; + }; + C5FFEA822ADDAD5B007282A2 /* SafariViewController */ = { + isa = PBXGroup; + children = ( + C5FFEA832ADDAD6D007282A2 /* SafariViewController.swift */, + ); + path = SafariViewController; + sourceTree = ""; + }; CF1A593129E5873D00AAC16B /* EchoUITests */ = { isa = PBXGroup; children = ( @@ -1950,12 +2055,13 @@ C5133A77294125CC00A8314C /* Web3 */, C55D349829630D440004314A /* Web3Wallet */, C5B2F7042970573D000DBA0E /* SolanaSwift */, - 84536D7329EEBCF0008EA8DB /* Web3Inbox */, A573C53C29EC366500E3CBFD /* HDWalletKit */, 8487A9452A836C3F0003D5AF /* Sentry */, A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */, 84943C7C2A9BA328007EBAC2 /* Mixpanel */, C5CFBED02AA9A31500378D41 /* Starscream */, + A59D25ED2AB3672700D7EA3A /* AsyncButton */, + A5F1526E2ACDC46B00D745A6 /* Web3ModalUI */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; @@ -2033,6 +2139,7 @@ C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */, 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */, 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */, + A5F1526D2ACDC46B00D745A6 /* XCRemoteSwiftPackageReference "web3modal-swift" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -2308,8 +2415,6 @@ C55D34B12965FB750004314A /* SessionProposalInteractor.swift in Sources */, C56EE247293F566D004840D1 /* ScanModule.swift in Sources */, C56EE28D293F5757004840D1 /* AppearanceConfigurator.swift in Sources */, - 84536D6E29EEAE1F008EA8DB /* Web3InboxModule.swift in Sources */, - 84536D7029EEAE28008EA8DB /* Web3InboxRouter.swift in Sources */, 847BD1D82989492500076C90 /* MainModule.swift in Sources */, 847BD1E7298A806800076C90 /* NotificationsInteractor.swift in Sources */, C56EE241293F566D004840D1 /* WalletModule.swift in Sources */, @@ -2320,29 +2425,34 @@ A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, C56EE250293F566D004840D1 /* ScanTargetView.swift in Sources */, C56EE28F293F5757004840D1 /* MigrationConfigurator.swift in Sources */, - 84B815552991217900FAD54E /* PushMessagesPresenter.swift in Sources */, A5A0844029D2F626000B9B17 /* DefaultCryptoProvider.swift in Sources */, 847BD1DD2989494F00076C90 /* TabPage.swift in Sources */, + A50D53C42ABA055700A4FD8B /* NotifyPreferencesInteractor.swift in Sources */, + A50D53C22ABA055700A4FD8B /* NotifyPreferencesPresenter.swift in Sources */, C55D348B295DD8CA0004314A /* PasteUriRouter.swift in Sources */, - 84B815572991217900FAD54E /* PushMessagesInteractor.swift in Sources */, 847BD1E4298A806800076C90 /* NotificationsModule.swift in Sources */, C55D348C295DD8CA0004314A /* PasteUriInteractor.swift in Sources */, 847BD1D92989492500076C90 /* MainPresenter.swift in Sources */, + C5FFEA842ADDAD6D007282A2 /* SafariViewController.swift in Sources */, C55D3497295DFA750004314A /* WelcomeView.swift in Sources */, A57879722A4F225E00F8D10B /* ImportAccount.swift in Sources */, C5B2F71029705827000DBA0E /* EthereumTransaction.swift in Sources */, + A50D53C32ABA055700A4FD8B /* NotifyPreferencesRouter.swift in Sources */, + A5D610C82AB31EE800C20083 /* SegmentedPicker.swift in Sources */, C56EE271293F56D7004840D1 /* View.swift in Sources */, + A5B4F7C62ABB20AE0099AF7C /* SubscriptionView.swift in Sources */, C5B2F6FD297055B0000DBA0E /* Signer.swift in Sources */, C56EE24D293F566D004840D1 /* WalletRouter.swift in Sources */, + A5D610D02AB35AD500C20083 /* ListingsNetworkService.swift in Sources */, C5F32A342954817600A6476E /* ConnectionDetailsView.swift in Sources */, C55D348A295DD8CA0004314A /* PasteUriPresenter.swift in Sources */, A51811A22A52E83100A52B15 /* SettingsInteractor.swift in Sources */, + A5B4F7C22ABB20AE0099AF7C /* SubscriptionPresenter.swift in Sources */, C56EE28E293F5757004840D1 /* ApplicationConfigurator.swift in Sources */, - 84B8155B2992A18D00FAD54E /* PushMessageViewModel.swift in Sources */, + 84B8155B2992A18D00FAD54E /* NotifyMessageViewModel.swift in Sources */, C55D347F295DD7140004314A /* AuthRequestModule.swift in Sources */, C56EE242293F566D004840D1 /* ScanPresenter.swift in Sources */, C56EE28B293F5757004840D1 /* SceneDelegate.swift in Sources */, - 84536D7229EEAE32008EA8DB /* Web3InboxViewController.swift in Sources */, C56EE276293F56D7004840D1 /* UIViewController.swift in Sources */, C56EE275293F56D7004840D1 /* InputConfig.swift in Sources */, C55D3493295DFA750004314A /* WelcomeModule.swift in Sources */, @@ -2355,10 +2465,11 @@ C56EE243293F566D004840D1 /* ScanView.swift in Sources */, 84310D05298BC980000C15B6 /* MainInteractor.swift in Sources */, C56EE288293F5757004840D1 /* ThirdPartyConfigurator.swift in Sources */, + C5FFEA7E2ADD8985007282A2 /* BrowserView.swift in Sources */, 847BD1D62989492500076C90 /* MainViewController.swift in Sources */, C5B2F6FA29705293000DBA0E /* SessionRequestInteractor.swift in Sources */, C55D34AE2965FB750004314A /* SessionProposalModule.swift in Sources */, - 84B815582991217900FAD54E /* PushMessagesView.swift in Sources */, + A5B4F7C42ABB20AE0099AF7C /* SubscriptionModule.swift in Sources */, C55D34B02965FB750004314A /* SessionProposalRouter.swift in Sources */, C55D3495295DFA750004314A /* WelcomeRouter.swift in Sources */, C5B2F6F729705293000DBA0E /* SessionRequestRouter.swift in Sources */, @@ -2366,41 +2477,54 @@ C55D34B22965FB750004314A /* SessionProposalView.swift in Sources */, C56EE248293F566D004840D1 /* ScanQR.swift in Sources */, 847BD1EB298A87AB00076C90 /* SubscriptionsViewModel.swift in Sources */, - 84B815542991217900FAD54E /* PushMessagesModule.swift in Sources */, C55D349B2965BC2F0004314A /* TagsView.swift in Sources */, 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */, C56EE289293F5757004840D1 /* Application.swift in Sources */, C56EE273293F56D7004840D1 /* UIColor.swift in Sources */, A51811982A52E21A00A52B15 /* ConfigurationService.swift in Sources */, C5F32A322954816C00A6476E /* ConnectionDetailsPresenter.swift in Sources */, - 84B815562991217900FAD54E /* PushMessagesRouter.swift in Sources */, + A5B4F7C32ABB20AE0099AF7C /* SubscriptionInteractor.swift in Sources */, + A50D53C52ABA055700A4FD8B /* NotifyPreferencesView.swift in Sources */, C56EE246293F566D004840D1 /* ScanRouter.swift in Sources */, + C5FFEA7A2ADD8974007282A2 /* BrowserRouter.swift in Sources */, C55D3481295DD7140004314A /* AuthRequestRouter.swift in Sources */, + C5FFEA812ADDACD7007282A2 /* WebView.swift in Sources */, C5B2F6F829705293000DBA0E /* SessionRequestView.swift in Sources */, C56EE28C293F5757004840D1 /* Configurator.swift in Sources */, + C5FFEA782ADD896E007282A2 /* BrowserPresenter.swift in Sources */, C55D3489295DD8CA0004314A /* PasteUriModule.swift in Sources */, C55D3494295DFA750004314A /* WelcomePresenter.swift in Sources */, C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, A57879712A4EDC8100F8D10B /* TextFieldView.swift in Sources */, + A5D610CA2AB3249100C20083 /* ListingViewModel.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, + A5D610CE2AB3594100C20083 /* ListingsAPI.swift in Sources */, 84943C7F2A9BA48C007EBAC2 /* ProfilingService.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, + A5D610D42AB35BED00C20083 /* FailableDecodable.swift in Sources */, + A56AC8F22AD88A5A001C8FAA /* Sequence.swift in Sources */, 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, + A50D53C12ABA055700A4FD8B /* NotifyPreferencesModule.swift in Sources */, + A5B4F7C52ABB20AE0099AF7C /* SubscriptionRouter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, C5B2F6FC297055B0000DBA0E /* SOLSigner.swift in Sources */, A518119F2A52E83100A52B15 /* SettingsModule.swift in Sources */, + C5FFEA7C2ADD897C007282A2 /* BrowserInteractor.swift in Sources */, 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */, C55D348D295DD8CA0004314A /* PasteUriView.swift in Sources */, C5F32A2C2954814200A6476E /* ConnectionDetailsModule.swift in Sources */, C56EE249293F566D004840D1 /* ScanInteractor.swift in Sources */, C56EE28A293F5757004840D1 /* AppDelegate.swift in Sources */, + C5FFEA762ADD8956007282A2 /* BrowserModule.swift in Sources */, C56EE2A3293F6BAF004840D1 /* UIPasteboardWrapper.swift in Sources */, C5B2F6F629705293000DBA0E /* SessionRequestModule.swift in Sources */, C56EE24E293F566D004840D1 /* WalletInteractor.swift in Sources */, A51811A32A52E83100A52B15 /* SettingsView.swift in Sources */, A57879732A4F248200F8D10B /* AccountStorage.swift in Sources */, C55D34AF2965FB750004314A /* SessionProposalPresenter.swift in Sources */, + A5D610D22AB35B1100C20083 /* Listings.swift in Sources */, + A5B4F7C82ABB21190099AF7C /* CacheAsyncImage.swift in Sources */, C5F32A302954816100A6476E /* ConnectionDetailsInteractor.swift in Sources */, 847BD1E8298A806800076C90 /* NotificationsView.swift in Sources */, ); @@ -2930,7 +3054,7 @@ INFOPLIST_KEY_UIRequiresFullScreen = NO; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2968,7 +3092,7 @@ INFOPLIST_KEY_UIRequiresFullScreen = NO; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.4; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3163,6 +3287,14 @@ version = 3.1.2; }; }; + A5F1526D2ACDC46B00D745A6 /* XCRemoteSwiftPackageReference "web3modal-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/WalletConnect/web3modal-swift"; + requirement = { + branch = develop; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3174,10 +3306,6 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnect; }; - 84536D7329EEBCF0008EA8DB /* Web3Inbox */ = { - isa = XCSwiftPackageProductDependency; - productName = Web3Inbox; - }; 8487A9432A836C2A0003D5AF /* Sentry */ = { isa = XCSwiftPackageProductDependency; package = 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */; @@ -3248,6 +3376,11 @@ isa = XCSwiftPackageProductDependency; productName = Web3Inbox; }; + A59D25ED2AB3672700D7EA3A /* AsyncButton */ = { + isa = XCSwiftPackageProductDependency; + package = A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */; + productName = AsyncButton; + }; A59F877528B5462900A9CD80 /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; @@ -3290,6 +3423,11 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectChat; }; + A5F1526E2ACDC46B00D745A6 /* Web3ModalUI */ = { + isa = XCSwiftPackageProductDependency; + package = A5F1526D2ACDC46B00D745A6 /* XCRemoteSwiftPackageReference "web3modal-swift" */; + productName = Web3ModalUI; + }; C5133A77294125CC00A8314C /* Web3 */ = { isa = XCSwiftPackageProductDependency; package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 60fae593d..600596394 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -162,6 +162,15 @@ "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", "version": "1.0.2" } + }, + { + "package": "swift-web3modal", + "repositoryURL": "https://github.com/WalletConnect/web3modal-swift", + "state": { + "branch": "develop", + "revision": "24602e2acee171fac26aa978b30666deec68a08f", + "version": null + } } ] }, diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme index 4f51ebe1b..351e9f253 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -147,11 +147,11 @@ + buildForTesting = "NO" + buildForRunning = "NO" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> RelayClient { @@ -33,8 +35,8 @@ final class HistoryTests: XCTestCase { logger: ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug)) } - private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryNetworkService { - let clientIdStorage = ClientIdStorage(keychain: keychain) + private func makeHistoryClient(defaults: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryNetworkService { + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychain, logger: logger) return HistoryNetworkService(clientIdStorage: clientIdStorage) } diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index 8c4e82d38..7a60a8b16 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -78,11 +78,14 @@ final class PairingTests: XCTestCase { let prefix = "🐶 Wallet: " let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) + let defaults = RuntimeKeyValueStorage() walletPairingClient = pairingClient let historyClient = HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", - keychain: keychain + keyValueStorage: defaults, + keychain: keychain, + logger: notifyLogger ) appAuthClient = AuthClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index ff358cb99..a0bfed40e 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -14,11 +14,9 @@ import WalletConnectSigner final class NotifyTests: XCTestCase { - var walletPairingClient: PairingClient! + var walletNotifyClientA: NotifyClient! - var walletNotifyClient: NotifyClient! - - let gmDappUrl = "https://notify.gm.walletconnect.com/" + let gmDappDomain = InputConfig.gmDappHost let pk = try! EthereumPrivateKey() @@ -39,6 +37,7 @@ final class NotifyTests: XCTestCase { let relayLogger = ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug) let pairingLogger = ConsoleLogger(prefix: prefix + " [Pairing]", loggingLevel: .debug) let networkingLogger = ConsoleLogger(prefix: prefix + " [Networking]", loggingLevel: .debug) + let kmsLogger = ConsoleLogger(prefix: prefix + " [KMS]", loggingLevel: .debug) let relayClient = RelayClientFactory.create( relayHost: InputConfig.relayHost, @@ -52,7 +51,8 @@ final class NotifyTests: XCTestCase { relayClient: relayClient, logger: networkingLogger, keychainStorage: keychain, - keyValueStorage: keyValueStorage) + keyValueStorage: keyValueStorage, + kmsLogger: kmsLogger) let pairingClient = PairingClientFactory.create( logger: pairingLogger, @@ -65,106 +65,157 @@ final class NotifyTests: XCTestCase { return (pairingClient, networkingClient, keychain, keyValueStorage) } - func makeWalletClients() { - let prefix = "🦋 Wallet: " + func makeWalletClient(prefix: String = "🦋 Wallet: ") -> NotifyClient { let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) - walletPairingClient = pairingClient let pushClient = PushClientFactory.create(projectId: "", pushHost: "echo.walletconnect.com", + keyValueStorage: keyValueStorage, keychainStorage: keychain, environment: .sandbox) let keyserverURL = URL(string: "https://keys.walletconnect.com")! - walletNotifyClient = NotifyClientFactory.create(keyserverURL: keyserverURL, - logger: notifyLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - groupKeychainStorage: KeychainStorageMock(), - networkInteractor: networkingInteractor, - pairingRegisterer: pairingClient, - pushClient: pushClient, - crypto: DefaultCryptoProvider()) + let client = NotifyClientFactory.create(projectId: InputConfig.projectId, + keyserverURL: keyserverURL, + logger: notifyLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + groupKeychainStorage: KeychainStorageMock(), + networkInteractor: networkingInteractor, + pairingRegisterer: pairingClient, + pushClient: pushClient, + crypto: DefaultCryptoProvider(), + notifyHost: InputConfig.notifyHost) + return client } override func setUp() { - makeWalletClients() + walletNotifyClientA = makeWalletClient() } func testWalletCreatesSubscription() async { let expectation = expectation(description: "expects to create notify subscription") - let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) - walletNotifyClient.newSubscriptionPublisher - .sink { [unowned self] subscription in + walletNotifyClientA.subscriptionsPublisher + .sink { [unowned self] subscriptions in + guard let subscription = subscriptions.first else { return } Task(priority: .high) { - try! await walletNotifyClient.deleteSubscription(topic: subscription.topic) + try await walletNotifyClientA.deleteSubscription(topic: subscription.topic) expectation.fulfill() } }.store(in: &publishers) - try! await walletNotifyClient.register(account: account, onSign: sign) - try! await walletNotifyClient.subscribe(metadata: metadata, account: account) + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testNotifyWatchSubscriptions() async throws { + let expectation = expectation(description: "expects client B to receive subscription created by client A") + expectation.assertForOverFulfill = false + + let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ") + clientB.subscriptionsPublisher.sink { subscriptions in + guard let subscription = subscriptions.first else { return } + Task(priority: .high) { + try await clientB.deleteSubscription(topic: subscription.topic) + expectation.fulfill() + } + }.store(in: &publishers) + + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + try! await clientB.register(account: account, domain: gmDappDomain, onSign: sign) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testNotifySubscriptionChanged() async throws { + let expectation = expectation(description: "expects client B to receive subscription after both clients are registered and client A creates one") + expectation.assertForOverFulfill = false + + let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ") + clientB.subscriptionsPublisher.sink { subscriptions in + guard let subscription = subscriptions.first else { return } + Task(priority: .high) { + try await clientB.deleteSubscription(topic: subscription.topic) + expectation.fulfill() + } + }.store(in: &publishers) + + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await clientB.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testWalletCreatesAndUpdatesSubscription() async { let expectation = expectation(description: "expects to create and update notify subscription") - let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) - let updateScope: Set = ["alerts"] - try! await walletNotifyClient.register(account: account, onSign: sign) - try! await walletNotifyClient.subscribe(metadata: metadata, account: account) - walletNotifyClient.newSubscriptionPublisher - .sink { [unowned self] subscription in - Task(priority: .high) { - try! await walletNotifyClient.update(topic: subscription.topic, scope: updateScope) + let updateScope: Set = ["8529aae8-cb26-4d49-922e-eb099044bebe"] + expectation.assertForOverFulfill = false + + var didUpdate = false + walletNotifyClientA.subscriptionsPublisher + .sink { [unowned self] subscriptions in + guard let subscription = subscriptions.first else { return } + let updatedScope = Set(subscription.scope.filter { $0.value.enabled == true }.keys) + + if !didUpdate { + didUpdate = true + Task(priority: .high) { + try await walletNotifyClientA.update(topic: subscription.topic, scope: updateScope) + } } - } - .store(in: &publishers) - - walletNotifyClient.updateSubscriptionPublisher - .sink { [unowned self] subscription in - let updatedScope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) - XCTAssertEqual(updatedScope, updateScope) - Task(priority: .high) { - try! await walletNotifyClient.deleteSubscription(topic: subscription.topic) - expectation.fulfill() + if updateScope == updatedScope { + Task(priority: .high) { + try await walletNotifyClientA.deleteSubscription(topic: subscription.topic) + expectation.fulfill() + } } }.store(in: &publishers) + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testNotifyServerSubscribeAndNotifies() async throws { let subscribeExpectation = expectation(description: "creates notify subscription") let messageExpectation = expectation(description: "receives a notify message") - let notifyMessage = NotifyMessage.stub() - - let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) - try! await walletNotifyClient.register(account: account, onSign: sign) - try! await walletNotifyClient.subscribe(metadata: metadata, account: account) + let notifyMessage = NotifyMessage.stub(type: "8529aae8-cb26-4d49-922e-eb099044bebe") - walletNotifyClient.newSubscriptionPublisher - .sink { subscription in + var didNotify = false + walletNotifyClientA.subscriptionsPublisher + .sink { subscriptions in + guard let subscription = subscriptions.first else { return } let notifier = Publisher() - Task(priority: .high) { - try await notifier.notify(topic: subscription.topic, account: subscription.account, message: notifyMessage) - subscribeExpectation.fulfill() + if !didNotify { + didNotify = true + Task(priority: .high) { + try await notifier.notify(topic: subscription.topic, account: subscription.account, message: notifyMessage) + subscribeExpectation.fulfill() + } } }.store(in: &publishers) - walletNotifyClient.notifyMessagePublisher + walletNotifyClientA.notifyMessagePublisher .sink { [unowned self] notifyMessageRecord in XCTAssertEqual(notifyMessage, notifyMessageRecord.message) Task(priority: .high) { - try await walletNotifyClient.deleteSubscription(topic: notifyMessageRecord.topic) + try await walletNotifyClientA.deleteSubscription(topic: notifyMessageRecord.topic) messageExpectation.fulfill() } }.store(in: &publishers) + try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) + try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) + wait(for: [subscribeExpectation, messageExpectation], timeout: InputConfig.defaultTimeout) } + } diff --git a/Example/IntegrationTests/Push/Publisher.swift b/Example/IntegrationTests/Push/Publisher.swift index 5ee993759..4b7165793 100644 --- a/Example/IntegrationTests/Push/Publisher.swift +++ b/Example/IntegrationTests/Push/Publisher.swift @@ -3,7 +3,7 @@ import Foundation class Publisher { func notify(topic: String, account: Account, message: NotifyMessage) async throws { - let url = URL(string: "https://\(InputConfig.castHost)/\(InputConfig.gmDappProjectId)/notify")! + let url = URL(string: "https://\(InputConfig.notifyHost)/\(InputConfig.gmDappProjectId)/notify")! var request = URLRequest(url: url) let notifyRequestPayload = NotifyRequest(notification: message, accounts: [account]) let encoder = JSONEncoder() diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift index 1ad6880ee..18387013b 100644 --- a/Example/IntegrationTests/Stubs/PushMessage.swift +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -2,12 +2,12 @@ import Foundation import WalletConnectNotify extension NotifyMessage { - static func stub() -> NotifyMessage { + static func stub(type: String) -> NotifyMessage { return NotifyMessage( title: "swift_test", - body: "gm_hourly", + body: "body", icon: "https://images.unsplash.com/photo-1581224463294-908316338239?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80", url: "https://web3inbox.com", - type: "private") + type: type) } } diff --git a/Example/PNDecryptionService/NotificationService.swift b/Example/PNDecryptionService/NotificationService.swift index 21da33666..50023dd13 100644 --- a/Example/PNDecryptionService/NotificationService.swift +++ b/Example/PNDecryptionService/NotificationService.swift @@ -25,7 +25,7 @@ class NotificationService: UNNotificationServiceExtension { catch { NSLog("Push decryption, error=%@", error.localizedDescription) bestAttemptContent.title = "" - bestAttemptContent.body = "content not set" + bestAttemptContent.body = error.localizedDescription } contentHandler(bestAttemptContent) } diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index d607b9607..72e63162b 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -31,7 +31,9 @@ final class RelayClientEndToEndTests: XCTestCase { private var publishers = Set() func makeRelayClient(prefix: String) -> RelayClient { - let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) + let keyValueStorage = RuntimeKeyValueStorage() + let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: KeychainStorageMock(), logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, url: InputConfig.relayUrl @@ -43,7 +45,6 @@ final class RelayClientEndToEndTests: XCTestCase { ) let socket = WebSocket(url: urlFactory.create(fallback: false)) let webSocketFactory = WebSocketFactoryMock(webSocket: socket) - let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, @@ -51,7 +52,6 @@ final class RelayClientEndToEndTests: XCTestCase { logger: logger ) let keychain = KeychainStorageMock() - let keyValueStorage = RuntimeKeyValueStorage() let relayClient = RelayClientFactory.create( relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, diff --git a/Example/Shared/DefaultSocketFactory.swift b/Example/Shared/DefaultSocketFactory.swift index ecb95f2a1..1f90167f8 100644 --- a/Example/Shared/DefaultSocketFactory.swift +++ b/Example/Shared/DefaultSocketFactory.swift @@ -11,6 +11,9 @@ extension WebSocket: WebSocketConnecting { struct DefaultSocketFactory: WebSocketFactory { func create(with url: URL) -> WebSocketConnecting { - return WebSocket(url: url) + let socket = WebSocket(url: url) + let queue = DispatchQueue(label: "com.walletconnect.sdk.sockets", attributes: .concurrent) + socket.callbackQueue = queue + return socket } } diff --git a/Example/Shared/Tests/InputConfig.swift b/Example/Shared/Tests/InputConfig.swift index e32975741..b420b93b8 100644 --- a/Example/Shared/Tests/InputConfig.swift +++ b/Example/Shared/Tests/InputConfig.swift @@ -6,14 +6,14 @@ struct InputConfig { return config(for: "RELAY_HOST")! } - static var castHost: String { - return config(for: "CAST_HOST")! - } - static var gmDappProjectId: String { return config(for: "GM_DAPP_PROJECT_ID")! } + static var gmDappHost: String { + return config(for: "GM_DAPP_HOST")! + } + static var gmDappProjectSecret: String { return config(for: "GM_DAPP_PROJECT_SECRET")! } @@ -22,6 +22,10 @@ struct InputConfig { return config(for: "JS_CLIENT_API_HOST")! } + static var notifyHost: String { + return config(for: "CAST_HOST")! + } + static var relayUrl: String { return "wss://\(relayHost)" } diff --git a/Example/WalletApp/ApplicationLayer/AppDelegate.swift b/Example/WalletApp/ApplicationLayer/AppDelegate.swift index 0b8afd190..1bef78e52 100644 --- a/Example/WalletApp/ApplicationLayer/AppDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/AppDelegate.swift @@ -1,6 +1,6 @@ import UIKit -import Web3Inbox import Combine +import WalletConnectNotify @main final class AppDelegate: UIResponder, UIApplicationDelegate { @@ -29,7 +29,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { UserDefaults.standard.set(deviceTokenString.joined(), forKey: "deviceToken") Task(priority: .high) { - try await Web3Inbox.instance.register(deviceToken: deviceToken) + try await Notify.instance.register(deviceToken: deviceToken) } } diff --git a/Example/WalletApp/ApplicationLayer/Application.swift b/Example/WalletApp/ApplicationLayer/Application.swift index 4015be2e3..95e3a6ab8 100644 --- a/Example/WalletApp/ApplicationLayer/Application.swift +++ b/Example/WalletApp/ApplicationLayer/Application.swift @@ -2,7 +2,7 @@ import Foundation import WalletConnectChat final class Application { - var uri: String? + var uri: WalletConnectURI? var requestSent = false lazy var pushRegisterer = PushRegisterer() @@ -11,4 +11,3 @@ final class Application { lazy var messageSigner = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() lazy var configurationService = ConfigurationService() } - diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index e4f5d9019..94176ec23 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -1,6 +1,7 @@ -import Foundation +import UIKit +import WalletConnectNetworking +import WalletConnectNotify -import Web3Inbox import Web3Wallet final class ConfigurationService { @@ -18,21 +19,30 @@ final class ConfigurationService { Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) - Web3Inbox.configure( - account: importAccount.account, - bip44: DefaultBIP44Provider(), - config: [.chatEnabled: false, .settingsEnabled: false], + Notify.configure( groupIdentifier: "group.com.walletconnect.sdk", environment: BuildConfiguration.shared.apnsEnvironment, - crypto: DefaultCryptoProvider(), - onSign: importAccount.onSign + crypto: DefaultCryptoProvider() ) - Web3Inbox.instance.setLogging(level: .debug) + + Notify.instance.setLogging(level: .debug) if let clientId = try? Networking.interactor.getClientId() { LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId) ProfilingService.instance.setUpProfiling(account: importAccount.account.absoluteString, clientId: clientId) } LoggingService.instance.startLogging() + + Task { + do { + try await Notify.instance.register(account: importAccount.account, domain: "com.walletconnect", onSign: importAccount.onSign) + } catch { + DispatchQueue.main.async { + let logMessage = LogMessage(message: "Push Server registration failed with: \(error.localizedDescription)") + ProfilingService.instance.send(logMessage: logMessage) + UIApplication.currentWindow.rootViewController?.showAlert(title: "Register error", error: error) + } + } + } } } diff --git a/Example/WalletApp/ApplicationLayer/Configurator/AppearanceConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/AppearanceConfigurator.swift index 793e9a835..ca41f7f63 100644 --- a/Example/WalletApp/ApplicationLayer/Configurator/AppearanceConfigurator.swift +++ b/Example/WalletApp/ApplicationLayer/Configurator/AppearanceConfigurator.swift @@ -3,15 +3,6 @@ import UIKit struct AppearanceConfigurator: Configurator { func configure() { - let appearance = UINavigationBarAppearance() - appearance.backgroundColor = .w_background - appearance.shadowColor = .clear - appearance.titleTextAttributes = [ - .foregroundColor: UIColor.w_foreground - ] - UINavigationBar.appearance().standardAppearance = appearance - UINavigationBar.appearance().scrollEdgeAppearance = appearance - UINavigationBar.appearance().compactAppearance = appearance } } diff --git a/Example/WalletApp/ApplicationLayer/ProfilingService.swift b/Example/WalletApp/ApplicationLayer/ProfilingService.swift index 14a778061..bfffe219a 100644 --- a/Example/WalletApp/ApplicationLayer/ProfilingService.swift +++ b/Example/WalletApp/ApplicationLayer/ProfilingService.swift @@ -2,7 +2,7 @@ import Foundation import Mixpanel import WalletConnectNetworking import Combine -import Web3Inbox +import WalletConnectNotify final class ProfilingService { public static var instance = ProfilingService() @@ -32,7 +32,8 @@ final class ProfilingService { mixpanel.people.set(properties: ["$name": account, "account": account]) handleLogs(from: Networking.instance.logsPublisher) - handleLogs(from: Web3Inbox.instance.logsPublisher) + handleLogs(from: Notify.instance.logsPublisher) + handleLogs(from: Push.instance.logsPublisher) } private func handleLogs(from publisher: AnyPublisher) { diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index f043c84d5..4a98cfa19 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -28,7 +28,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() - app.uri = connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?uri=", with: "") + app.uri = WalletConnectURI(connectionOptions: connectionOptions) app.requestSent = (connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?", with: "") == "requestSent") configurators.configure() @@ -37,11 +37,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first else { return } - let queryParams = context.url.queryParameters + let uri = WalletConnectURI(urlContext: context) - if let uri = queryParams["uri"] as? String { + if let uri { Task { - try await Pair.instance.pair(uri: WalletConnectURI(string: uri)!) + try await Pair.instance.pair(uri: uri) } } } diff --git a/Example/WalletApp/BusinessLayer/ListingsSertice/Listings.swift b/Example/WalletApp/BusinessLayer/ListingsSertice/Listings.swift new file mode 100644 index 000000000..4ab2673c9 --- /dev/null +++ b/Example/WalletApp/BusinessLayer/ListingsSertice/Listings.swift @@ -0,0 +1,20 @@ +import Foundation + +struct Listing: Codable { + struct ImageURL: Codable { + @FailableDecodable + private(set) var sm: URL? + + @FailableDecodable + private(set) var md: URL? + + @FailableDecodable + private(set) var lg: URL? + } + let id: String + let name: String + let description: String? + let homepage: String? + let image_url: ImageURL? + let dapp_url: String +} diff --git a/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift new file mode 100644 index 000000000..8e3700db7 --- /dev/null +++ b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift @@ -0,0 +1,31 @@ +import Foundation +import HTTPClient + +enum ListingsAPI: HTTPService { + + var path: String { + return "/w3i/v1/projects" + } + + var method: HTTPMethod { + return .get + } + + var body: Data? { + return nil + } + + var queryParameters: [String : String]? { + return ["projectId": InputConfig.projectId, "entries": "100", "is_verified": "false"] + } + + var additionalHeaderFields: [String : String]? { + return nil + } + + var scheme: String { + return "https" + } + + case notifyDApps +} diff --git a/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsNetworkService.swift b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsNetworkService.swift new file mode 100644 index 000000000..cd0676d9e --- /dev/null +++ b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsNetworkService.swift @@ -0,0 +1,15 @@ +import Foundation +import HTTPClient + +final class ListingsNetworkService { + + struct ListingsResponse: Codable { + let projects: [String: Listing] + } + + func getListings() async throws -> [Listing] { + let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") + let response = try await httpClient.request(ListingsResponse.self, at: ListingsAPI.notifyDApps) + return response.projects.values.compactMap { $0 } + } +} diff --git a/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift b/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift new file mode 100644 index 000000000..b1b86da7d --- /dev/null +++ b/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift @@ -0,0 +1,21 @@ +import Foundation + +extension Sequence { + func sorted( + by firstPredicate: (Element, Element) -> Bool, + _ secondPredicate: (Element, Element) -> Bool, + _ otherPredicates: ((Element, Element) -> Bool)... + ) -> [Element] { + return sorted(by:) { lhs, rhs in + if firstPredicate(lhs, rhs) { return true } + if firstPredicate(rhs, lhs) { return false } + if secondPredicate(lhs, rhs) { return true } + if secondPredicate(rhs, lhs) { return false } + for predicate in otherPredicates { + if predicate(lhs, rhs) { return true } + if predicate(rhs, lhs) { return false } + } + return false + } + } +} diff --git a/Example/WalletApp/Common/Extensions/UIKit/UIColor.swift b/Example/WalletApp/Common/Extensions/UIKit/UIColor.swift index 1f175e66e..7d197cbef 100644 --- a/Example/WalletApp/Common/Extensions/UIKit/UIColor.swift +++ b/Example/WalletApp/Common/Extensions/UIKit/UIColor.swift @@ -17,4 +17,13 @@ extension UIColor { blue: rgb & 0xFF ) } + + func image(size: CGSize = CGSize(width: 1, height: 1)) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 0) + setFill() + UIRectFill(CGRect(origin: CGPoint.zero, size: size)) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } } diff --git a/Example/WalletApp/Common/Extensions/UIKit/UIViewController.swift b/Example/WalletApp/Common/Extensions/UIKit/UIViewController.swift index c058d9ef6..c9ab837d4 100644 --- a/Example/WalletApp/Common/Extensions/UIKit/UIViewController.swift +++ b/Example/WalletApp/Common/Extensions/UIKit/UIViewController.swift @@ -48,6 +48,12 @@ extension UIViewController { navigationController.navigationBar.prefersLargeTitles = true return navigationController } + + func showAlert(title: String, error: Error) { + let alert = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + present(alert, animated: true, completion: nil) + } } extension UIApplication { diff --git a/Example/WalletApp/Common/Helpers/CacheAsyncImage.swift b/Example/WalletApp/Common/Helpers/CacheAsyncImage.swift new file mode 100644 index 000000000..baafc2c98 --- /dev/null +++ b/Example/WalletApp/Common/Helpers/CacheAsyncImage.swift @@ -0,0 +1,52 @@ +import SwiftUI + +struct CacheAsyncImage: View where Content: View{ + + private let url: URL? + private let scale: CGFloat + private let transaction: Transaction + private let content: (AsyncImagePhase) -> Content + + init( + url: URL?, + scale: CGFloat = 1.0, + transaction: Transaction = Transaction(), + @ViewBuilder content: @escaping (AsyncImagePhase) -> Content + ){ + self.url = url + self.scale = scale + self.transaction = transaction + self.content = content + } + + var body: some View{ + if let url, let cached = ImageCache[url] { + content(.success(cached)) + } else { + AsyncImage( + url: url, + scale: scale, + transaction: transaction + ){ phase in + cacheAndRender(phase: phase) + } + } + } + func cacheAndRender(phase: AsyncImagePhase) -> some View{ + if case .success (let image) = phase, let url { + ImageCache[url] = image + } + return content(phase) + } +} +fileprivate class ImageCache{ + static private var cache: [URL: Image] = [:] + static subscript(url: URL) -> Image?{ + get{ + ImageCache.cache[url] + } + set{ + ImageCache.cache[url] = newValue + } + } +} diff --git a/Example/WalletApp/Common/Helpers/FailableDecodable.swift b/Example/WalletApp/Common/Helpers/FailableDecodable.swift new file mode 100644 index 000000000..f4274e35e --- /dev/null +++ b/Example/WalletApp/Common/Helpers/FailableDecodable.swift @@ -0,0 +1,18 @@ +@propertyWrapper +struct FailableDecodable: Codable, Hashable { + var wrappedValue: Wrapped? + + init(_ wrappedValue: Wrapped?) { + self.wrappedValue = wrappedValue + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try? container.decode(Wrapped.self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} diff --git a/Example/WalletApp/Common/Helpers/SegmentedPicker.swift b/Example/WalletApp/Common/Helpers/SegmentedPicker.swift new file mode 100644 index 000000000..a9906f378 --- /dev/null +++ b/Example/WalletApp/Common/Helpers/SegmentedPicker.swift @@ -0,0 +1,94 @@ +import SwiftUI + +public struct SegmentedPicker: View where Content: View, Selection: View { + + public typealias Data = [Element] + + @State private var frames: [CGRect] + @Binding private var selectedIndex: Data.Index? + + private let data: Data + private let selection: () -> Selection + private let content: (Data.Element, Bool) -> Content + + public init(_ data: Data, + selectedIndex: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selection: @escaping () -> Selection + ) { + self.data = data + self.content = content + self.selection = selection + self._selectedIndex = selectedIndex + self._frames = State(wrappedValue: Array(repeating: .zero, count: data.count)) + } + + public var body: some View { + ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment, vertical: .center)) { + + if let selectedIndex = selectedIndex { + selection() + .frame(width: frames[selectedIndex].width, + height: frames[selectedIndex].height) + .alignmentGuide(.horizontalCenterAlignment) { dimensions in + dimensions[HorizontalAlignment.center] + } + } + + HStack(spacing: 0) { + ForEach(data.indices, id: \.self) { index in + Button(action: { selectedIndex = index }, + label: { content(data[index], selectedIndex == index) } + ) + .buttonStyle(PlainButtonStyle()) + .background(GeometryReader { proxy in + Color.clear.onAppear { frames[index] = proxy.frame(in: .global) } + }) + .alignmentGuide(.horizontalCenterAlignment, + isActive: selectedIndex == index) { dimensions in + dimensions[HorizontalAlignment.center] + } + } + } + } + } +} + +extension HorizontalAlignment { + + private enum CenterAlignmentID: AlignmentID { + static func defaultValue(in dimension: ViewDimensions) -> CGFloat { + return dimension[HorizontalAlignment.center] + } + } + + static var horizontalCenterAlignment: HorizontalAlignment { + HorizontalAlignment(CenterAlignmentID.self) + } +} + +extension View { + @ViewBuilder + @inlinable func alignmentGuide(_ alignment: HorizontalAlignment, + isActive: Bool, + computeValue: @escaping (ViewDimensions) -> CGFloat + ) -> some View { + if isActive { + alignmentGuide(alignment, computeValue: computeValue) + } else { + self + } + } + + @ViewBuilder + @inlinable func alignmentGuide(_ alignment: VerticalAlignment, + isActive: Bool, + computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View + { + if isActive { + alignmentGuide(alignment, computeValue: computeValue) + } else { + self + } + } +} diff --git a/Example/WalletApp/Common/VIPER/SceneViewController.swift b/Example/WalletApp/Common/VIPER/SceneViewController.swift index 0c990baf7..33b846ed3 100644 --- a/Example/WalletApp/Common/VIPER/SceneViewController.swift +++ b/Example/WalletApp/Common/VIPER/SceneViewController.swift @@ -1,7 +1,8 @@ import SwiftUI enum NavigationBarStyle { - case translucent(UIColor) + case solid(UIColor) + case clear } protocol SceneViewModel { @@ -29,7 +30,7 @@ extension SceneViewModel { return .none } var navigationBarStyle: NavigationBarStyle { - return .translucent(.w_background) + return .solid(.w_background) } var preferredStatusBarStyle: UIStatusBarStyle { return .default @@ -55,6 +56,10 @@ class SceneViewController: UIHostingCo super.viewDidLoad() setupView() setupNavigation() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) setupNavigationBarStyle() } @@ -79,9 +84,18 @@ private extension SceneViewController { func setupNavigationBarStyle() { switch viewModel.navigationBarStyle { - case .translucent(let color): + case .solid(let color): + navigationController?.navigationBar.setBackgroundImage(color.image(), for: .default) + navigationController?.navigationBar.isTranslucent = false + navigationController?.navigationBar.backgroundColor = color navigationController?.navigationBar.barTintColor = color + case .clear: + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.shadowImage = UIImage() navigationController?.navigationBar.isTranslucent = true + navigationController?.navigationBar.backgroundColor = .clear + navigationController?.navigationBar.barTintColor = .clear + navigationController?.navigationBar.tintColor = .w_foreground } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift index 4a9cae866..f1fe362ef 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -9,7 +9,7 @@ final class AuthRequestPresenter: ObservableObject { let importAccount: ImportAccount let request: AuthRequest - let verified: Bool? + let validationStatus: VerifyContext.ValidationStatus? var message: String { return interactor.formatted(request: request, account: importAccount.account) @@ -29,7 +29,7 @@ final class AuthRequestPresenter: ObservableObject { self.router = router self.importAccount = importAccount self.request = request - self.verified = (context?.validation == .valid) ? true : (context?.validation == .unknown ? nil : false) + self.validationStatus = context?.validation } @MainActor diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift index 067a6b00e..97d40b444 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift @@ -17,88 +17,83 @@ struct AuthRequestView: View { .resizable() .scaledToFit() - HStack { - Text(presenter.request.payload.domain) - .foregroundColor(.grey8) - .font(.system(size: 22, weight: .bold, design: .rounded)) - - if let verified = presenter.verified { - if verified { - Image(systemName: "checkmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - } else { - Image(systemName: "xmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - } - } else { - Image(systemName: "exclamationmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .orange) - } - } - .padding(.top, 10) + Text(presenter.request.payload.domain) + .foregroundColor(.grey8) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .padding(.top, 10) Text("would like to connect") .foregroundColor(.grey8) .font(.system(size: 22, weight: .medium, design: .rounded)) - Text(presenter.request.payload.domain) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.request.payload.domain) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } .padding(.top, 8) + } else { + Text(presenter.request.payload.domain) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } + + switch presenter.validationStatus { + case .unknown: + verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) + + case .invalid: + verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) + + case .scam: + verifyBadgeView(imageName: "exclamationmark.shield.fill", title: "Security risk", color: .red) + + default: + EmptyView() + } authRequestView() - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + Group { + switch presenter.validationStatus { + case .invalid: + verifyDescriptionView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .red) + + case .unknown: + verifyDescriptionView(imageName: "exclamationmark.circle.fill", title: "Unknown domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .orange) + + case .scam: + verifyDescriptionView(imageName: "exclamationmark.shield.fill", title: "Security risk", description: "This website is flagged as unsafe by multiple security providers. Leave immediately to protect your assets.", color: .red) + + default: + EmptyView() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + } + + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() + } + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) + + } .padding(20) .background(.ultraThinMaterial) @@ -132,18 +127,111 @@ struct AuthRequestView: View { } .padding(.horizontal, 18) .padding(.vertical, 10) - .frame(height: 250) + .frame(height: 150) } .background(Color.whiteBackground) .cornerRadius(20, corners: .allCorners) .padding(.horizontal, 5) .padding(.bottom, 5) - } .background(.thinMaterial) .cornerRadius(25, corners: .allCorners) } - .padding(.top, 30) + .padding(.vertical, 30) + } + + private func verifyBadgeView(imageName: String, title: String, color: Color) -> some View { + HStack(spacing: 5) { + Image(systemName: imageName) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(color) + + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + } + .padding(5) + .background(color.opacity(0.15)) + .cornerRadius(10) + .padding(.top, 8) + } + + private func verifyDescriptionView(imageName: String, title: String, description: String, color: Color) -> some View { + HStack(spacing: 15) { + Image(systemName: imageName) + .font(.system(size: 20, design: .rounded)) + .foregroundColor(color) + + VStack(alignment: .leading, spacing: 5) { + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + Text(description) + .foregroundColor(.grey8) + .font(.system(size: 14, weight: .medium, design: .rounded)) + } + } + .frame(maxWidth: .infinity) + .padding() + .background(color.opacity(0.15)) + .cornerRadius(20) + } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserInteractor.swift new file mode 100644 index 000000000..3134e266e --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserInteractor.swift @@ -0,0 +1,3 @@ +final class BrowserInteractor { + +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserModule.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserModule.swift new file mode 100644 index 000000000..bcbd6240f --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserModule.swift @@ -0,0 +1,16 @@ +import SwiftUI + +final class BrowserModule { + @discardableResult + static func create(app: Application) -> UIViewController { + let router = BrowserRouter(app: app) + let interactor = BrowserInteractor() + let presenter = BrowserPresenter(interactor: interactor, router: router) + let view = BrowserView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + router.viewController = viewController + + return viewController + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserPresenter.swift new file mode 100644 index 000000000..a3ffcc4cc --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserPresenter.swift @@ -0,0 +1,62 @@ +import UIKit +import Combine +import WebKit + +import WalletConnectNetworking + +final class BrowserPresenter: ObservableObject { + private let interactor: BrowserInteractor + private let router: BrowserRouter + + weak var webView: WKWebView? + + @Published var urlString = "https://react-app.walletconnect.com" + + private var disposeBag = Set() + + init(interactor: BrowserInteractor, router: BrowserRouter) { + defer { setupInitialState() } + self.interactor = interactor + self.router = router + } + + func loadURLString() { + if let url = URL(string: urlString) { + webView?.load(URLRequest(url: url.sanitise)) + } + } + + func reload() { + webView?.reload() + } +} + +// MARK: SceneViewModel +extension BrowserPresenter: SceneViewModel { + var sceneTitle: String? { + return "Browser" + } + + var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { + return .always + } +} + +// MARK: Privates +private extension BrowserPresenter { + func setupInitialState() { + + } +} + +extension URL { + var sanitise: URL { + if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) { + if components.scheme == nil { + components.scheme = "https" + } + return components.url ?? self + } + return self + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserRouter.swift similarity index 58% rename from Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxRouter.swift rename to Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserRouter.swift index 3631c35be..e0b1d5883 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserRouter.swift @@ -1,7 +1,6 @@ import UIKit -final class Web3InboxRouter { - +final class BrowserRouter { weak var viewController: UIViewController! private let app: Application @@ -9,4 +8,8 @@ final class Web3InboxRouter { init(app: Application) { self.app = app } + + func presentWelcome() { + BrowserModule.create(app: app).present() + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserView.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserView.swift new file mode 100644 index 000000000..082bd420c --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/BrowserView.swift @@ -0,0 +1,66 @@ +import SwiftUI + +struct BrowserView: View { + @EnvironmentObject var viewModel: BrowserPresenter + + @State private var selectedBrowser = 0 + + var body: some View { + VStack { + Picker("", selection: $selectedBrowser) { + Text("WKWebView").tag(0) + Text("SafariViewController").tag(1) + } + .pickerStyle(.segmented) + .padding() + + ZStack { + if selectedBrowser == 0 { + VStack { + HStack { + TextField( + viewModel.urlString, + text: $viewModel.urlString + ) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onSubmit { + viewModel.loadURLString() + } + + Button { + viewModel.reload() + } label: { + Image(systemName: "arrow.clockwise") + } + } + .padding(.horizontal) + + Divider() + .padding(.top) + + if let url = URL(string: viewModel.urlString) { + WebView(url: url, viewModel: viewModel) + } + + Spacer() + } + .onAppear { + viewModel.loadURLString() + } + } else { + if let url = URL(string: viewModel.urlString) { + SafariWebView(url: url.sanitise) + } + } + } + } + } +} + +#if DEBUG +struct BrowserView_Previews: PreviewProvider { + static var previews: some View { + BrowserView() + } +} +#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/SafariViewController/SafariViewController.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/SafariViewController/SafariViewController.swift new file mode 100644 index 000000000..16844098e --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/SafariViewController/SafariViewController.swift @@ -0,0 +1,14 @@ +import SwiftUI +import SafariServices + +struct SafariWebView: UIViewControllerRepresentable { + let url: URL + + func makeUIViewController(context: Context) -> SFSafariViewController { + return SFSafariViewController(url: url) + } + + func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { + + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Browser/WebView/WebView.swift b/Example/WalletApp/PresentationLayer/Wallet/Browser/WebView/WebView.swift new file mode 100644 index 000000000..a78105312 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Browser/WebView/WebView.swift @@ -0,0 +1,19 @@ +import SwiftUI +import WebKit + +struct WebView: UIViewRepresentable { + let url: URL + + @ObservedObject var viewModel: BrowserPresenter + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + viewModel.webView = webView + webView.load(URLRequest(url: url)) + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 4be0dc58a..451cce287 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -16,8 +16,8 @@ final class MainPresenter { var viewControllers: [UIViewController] { return [ router.walletViewController(importAccount: importAccount), - router.notificationsViewController(), - router.web3InboxViewController(), + router.notificationsViewController(importAccount: importAccount), + router.browserViewController(), router.settingsViewController() ] } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index 7d176926f..6e67c078c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -17,13 +17,8 @@ final class MainRouter { .wrapToNavigationController() } - func notificationsViewController() -> UIViewController { - return NotificationsModule.create(app: app) - .wrapToNavigationController() - } - - func web3InboxViewController() -> UIViewController { - return Web3InboxModule.create(app: app) + func notificationsViewController(importAccount: ImportAccount) -> UIViewController { + return NotificationsModule.create(app: app, importAccount: importAccount) .wrapToNavigationController() } @@ -32,6 +27,11 @@ final class MainRouter { .wrapToNavigationController() } + func browserViewController() -> UIViewController { + return BrowserModule.create(app: app) + .wrapToNavigationController() + } + func present(proposal: Session.Proposal, importAccount: ImportAccount, context: VerifyContext?) { SessionProposalModule.create(app: app, importAccount: importAccount, proposal: proposal, context: context) .presentFullScreen(from: viewController, transparentBackground: true) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift index 3f2051baf..d79c5a4e4 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift @@ -3,7 +3,7 @@ import UIKit enum TabPage: CaseIterable { case wallet case notifications - case web3Inbox + case browser case settings var title: String { @@ -12,10 +12,10 @@ enum TabPage: CaseIterable { return "Apps" case .notifications: return "Notifications" - case .web3Inbox: - return "Web3Inbox" case .settings: return "Settings" + case .browser: + return "Browser" } } @@ -25,8 +25,8 @@ enum TabPage: CaseIterable { return UIImage(systemName: "house.fill")! case .notifications: return UIImage(systemName: "bell.fill")! - case .web3Inbox: - return UIImage(systemName: "bell.fill")! + case .browser: + return UIImage(systemName: "network")! case .settings: return UIImage(systemName: "gearshape.fill")! } @@ -37,6 +37,6 @@ enum TabPage: CaseIterable { } static var enabledTabs: [TabPage] { - return [.wallet, .notifications, .web3Inbox, .settings] + return [.wallet, .notifications, .browser, .settings] } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/ListingViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/ListingViewModel.swift new file mode 100644 index 000000000..08286926d --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/ListingViewModel.swift @@ -0,0 +1,27 @@ +import Foundation + +struct ListingViewModel: Identifiable { + + let listing: Listing + + var id: String { + return listing.id + } + + var imageUrl: URL? { + return listing.image_url?.md + } + + var title: String { + return listing.name + } + + var subtitle: String { + return listing.description ?? "" + } + + var appDomain: String? { + let url = listing.dapp_url + return URL(string: url)?.host + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift index e727c94e3..6e0f12f42 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift @@ -1,22 +1,50 @@ import Foundation import WalletConnectNotify +typealias SubscriptionScope = [String: ScopeValue] + struct SubscriptionsViewModel: Identifiable { let subscription: NotifySubscription + let messages: [NotifyMessageRecord]? + + init(subscription: NotifySubscription, messages: [NotifyMessageRecord]? = nil) { + self.subscription = subscription + self.messages = messages + } var id: String { return subscription.topic } - var imageUrl: String { - return subscription.metadata.url + var imageUrl: URL? { + return try? subscription.metadata.icons.first?.asURL() } - var title: String { + var subtitle: String { + return subscription.metadata.description + } + + var name: String { return subscription.metadata.name } - var subtitle: String { + var description: String { return subscription.metadata.description } + + var domain: String { + return subscription.metadata.url + } + + var scope: SubscriptionScope { + return subscription.scope + } + + var messagesCount: Int { + return messages?.count ?? 0 + } + + var hasMessage: Bool { + return messagesCount != 0 + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift index 84797d283..3eeeb8ea3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift @@ -1,3 +1,4 @@ +import Foundation import WalletConnectNotify import Combine @@ -7,11 +8,22 @@ final class NotificationsInteractor { return Notify.instance.subscriptionsPublisher } + private let importAccount: ImportAccount + + init(importAccount: ImportAccount) { + self.importAccount = importAccount + } + func getSubscriptions() -> [NotifySubscription] { - let subs = Notify.instance.getActiveSubscriptions() + let subs = Notify.instance.getActiveSubscriptions(account: importAccount.account) return subs } + func getListings() async throws -> [Listing] { + let service = ListingsNetworkService() + return try await service.getListings() + } + func removeSubscription(_ subscription: NotifySubscription) async { do { try await Notify.instance.deleteSubscription(topic: subscription.topic) @@ -19,4 +31,55 @@ final class NotificationsInteractor { print(error) } } + + func subscribe(domain: String) async throws { + return try await withCheckedThrowingContinuation { continuation in + var cancellable: AnyCancellable? + cancellable = subscriptionsPublisher + .setFailureType(to: Error.self) + .timeout(10, scheduler: RunLoop.main, customError: { Errors.subscribeTimeout }) + .sink(receiveCompletion: { completion in + defer { cancellable?.cancel() } + switch completion { + case .failure(let error): continuation.resume(with: .failure(error)) + case .finished: break + } + }, receiveValue: { subscriptions in + guard subscriptions.contains(where: { $0.metadata.url == domain }) else { return } + cancellable?.cancel() + continuation.resume(with: .success(())) + }) + + Task { [cancellable] in + do { + try await Notify.instance.subscribe(appDomain: domain, account: importAccount.account) + } catch { + cancellable?.cancel() + continuation.resume(throwing: error) + } + } + } + } + + func unsubscribe(topic: String) async throws { + try await Notify.instance.deleteSubscription(topic: topic) + } + + func messages(for subscription: NotifySubscription) -> [NotifyMessageRecord] { + return Notify.instance.getMessageHistory(topic: subscription.topic) + } +} + +private extension NotificationsInteractor { + + enum Errors: Error, LocalizedError { + case subscribeTimeout + + var errorDescription: String? { + switch self { + case .subscribeTimeout: + return "Subscribe method timeout" + } + } + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsModule.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsModule.swift index f7b2b620c..cbfdf2c41 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsModule.swift @@ -3,9 +3,9 @@ import SwiftUI final class NotificationsModule { @discardableResult - static func create(app: Application) -> UIViewController { + static func create(app: Application, importAccount: ImportAccount) -> UIViewController { let router = NotificationsRouter(app: app) - let interactor = NotificationsInteractor() + let interactor = NotificationsInteractor(importAccount: importAccount) let presenter = NotificationsPresenter(interactor: interactor, router: router) let view = NotificationsView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift index 0aa9d878d..91a573857 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import WalletConnectNotify final class NotificationsPresenter: ObservableObject { @@ -7,7 +8,26 @@ final class NotificationsPresenter: ObservableObject { private let router: NotificationsRouter private var disposeBag = Set() - @Published var subscriptions: [SubscriptionsViewModel] = [] + @Published private var subscriptions: [NotifySubscription] = [] + @Published private var listings: [Listing] = [] + + var subscriptionViewModels: [SubscriptionsViewModel] { + return subscriptions + .map { SubscriptionsViewModel(subscription: $0, messages: interactor.messages(for: $0)) } + .sorted(by: + { $0.messagesCount > $1.messagesCount }, + { $0.name < $1.name } + ) + } + + var listingViewModels: [ListingViewModel] { + return listings + .map { ListingViewModel(listing: $0) } + .sorted(by: + { subscription(forListing: $0) != nil && subscription(forListing: $1) == nil }, + { $0.title < $1.title } + ) + } init(interactor: NotificationsInteractor, router: NotificationsRouter) { defer { setupInitialState() } @@ -16,17 +36,40 @@ final class NotificationsPresenter: ObservableObject { } - func didPress(_ subscription: SubscriptionsViewModel) { + @MainActor + func fetch() async throws { + listings = try await interactor.getListings() + } + + func subscription(forListing listing: ListingViewModel) -> SubscriptionsViewModel? { + return subscriptionViewModels.first(where: { $0.domain == listing.appDomain }) + } + + func subscribe(listing: ListingViewModel) async throws { + if let domain = listing.appDomain { + try await interactor.subscribe(domain: domain) + } + } + + func unsubscribe(subscription: SubscriptionsViewModel) async throws { + try await interactor.unsubscribe(topic: subscription.subscription.topic) + } + + func didPress(subscription: SubscriptionsViewModel) { router.presentNotifications(subscription: subscription.subscription) } + func didPress(listing: ListingViewModel) { + + } + func setupInitialState() { setupSubscriptions() } func removeSubscribtion(at indexSet: IndexSet) async { if let index = indexSet.first { - await interactor.removeSubscription(subscriptions[index].subscription) + await interactor.removeSubscription(subscriptionViewModels[index].subscription) } } } @@ -35,7 +78,7 @@ final class NotificationsPresenter: ObservableObject { extension NotificationsPresenter: SceneViewModel { var sceneTitle: String? { - return "Notifications" + return "Inbox" } var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { @@ -49,14 +92,11 @@ private extension NotificationsPresenter { func setupSubscriptions() { self.subscriptions = interactor.getSubscriptions() - .map { - return SubscriptionsViewModel(subscription: $0) - } + interactor.subscriptionsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] notifySubscriptions in self?.subscriptions = notifySubscriptions - .map { SubscriptionsViewModel(subscription: $0) } } .store(in: &disposeBag) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift index eebcfd7b3..843fcc14d 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift @@ -12,7 +12,7 @@ final class NotificationsRouter { } func presentNotifications(subscription: NotifySubscription) { - PushMessagesModule.create(app: app, subscription: subscription) + SubscriptionModule.create(app: app, subscription: subscription) .push(from: viewController) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift index aed832740..73151545c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift @@ -1,100 +1,216 @@ import SwiftUI +import Web3ModalUI +import AsyncButton struct NotificationsView: View { @EnvironmentObject var presenter: NotificationsPresenter + @State var selectedIndex: Int = 0 + @ViewBuilder var body: some View { - ZStack { - Color.grey100 - .edgesIgnoringSafeArea(.all) - - VStack(alignment: .leading, spacing: 16) { - ZStack { - if presenter.subscriptions.isEmpty { - VStack(spacing: 10) { - Image(systemName: "bell.badge.fill") - .resizable() - .frame(width: 32, height: 32) - .aspectRatio(contentMode: .fit) - .foregroundColor(.grey50) - - Text("Notifications from connected apps will appear here. To enable notifications, visit the app in your browser and look for a \(Image(systemName: "bell.fill")) notifications toggle \(Image(systemName: "switch.2"))") - .foregroundColor(.grey50) - .font(.system(size: 15, weight: .regular, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) - } - .padding(20) + VStack(spacing: 0) { + PreventCollapseView() + List { + Section { + if selectedIndex == 0 { + notifications() + } else { + discover() } - - VStack { - if !presenter.subscriptions.isEmpty { - List { - ForEach(presenter.subscriptions, id: \.id) { subscription in - subscriptionsView(subscription: subscription) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)) - } - .onDelete { indexSet in - Task(priority: .high) { - await presenter.removeSubscribtion(at: indexSet) - } - } + } header: { + HStack { + SegmentedPicker(["Notifications", "Discover"], + selectedIndex: Binding( + get: { selectedIndex }, + set: { selectedIndex = $0 ?? 0 }), + content: { item, isSelected in + Text(item) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(isSelected ? Color.primary : Color.secondary ) + .padding(.trailing, 32) + .padding(.vertical, 8) + }, selection: { + VStack(spacing: 0) { + Spacer() + Rectangle() + .fill(.Blue100) + .frame(height: 2) + .padding(.trailing, 32) } - .listStyle(PlainListStyle()) - } + }) } + .animation(.easeInOut(duration: 0.3)) + .listRowBackground(Color.clear) } } - .padding(.vertical, 20) + .listStyle(PlainListStyle()) + } + .task { + try? await presenter.fetch() } } + private func discover() -> some View { + return ForEach(presenter.listingViewModels) { listing in + discoverListRow(listing: listing) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + } + + private func emptySubscriptionsView() -> some View { + VStack(spacing: 10) { + Spacer() + Image(systemName: "bell.badge.fill") + .resizable() + .frame(width: 32, height: 32) + .aspectRatio(contentMode: .fit) + .foregroundColor(.grey50) + Text("Notifications from connected apps will appear here. To enable notifications, visit the app in your browser and look for a \(Image(systemName: "bell.fill")) notifications toggle \(Image(systemName: "switch.2"))") + .foregroundColor(.grey50) + .font(.system(size: 15, weight: .regular, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + Spacer() + } + .padding(20) + } - private func subscriptionsView(subscription: SubscriptionsViewModel) -> some View { + private func notifications() -> some View { + ForEach(presenter.subscriptionViewModels, id: \.id) { subscription in + subscriptionRow(subscription: subscription) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 20, leading: 20, bottom: 0, trailing: 20)) + } + .onDelete { indexSet in + Task(priority: .high) { + await presenter.removeSubscribtion(at: indexSet) + } + } + } + + private func subscriptionRow(subscription: SubscriptionsViewModel) -> some View { Button { - presenter.didPress(subscription) + presenter.didPress(subscription: subscription) } label: { VStack { HStack(spacing: 10) { - AsyncImage(url: URL(string: subscription.imageUrl)) { phase in + CacheAsyncImage(url: subscription.imageUrl) { phase in if let image = phase.image { image .resizable() .frame(width: 60, height: 60) - .background(Color.black) + .background(Color.grey8.opacity(0.1)) .cornerRadius(30, corners: .allCorners) } else { - Color.black + Color.grey8.opacity(0.1) .frame(width: 60, height: 60) .cornerRadius(30, corners: .allCorners) } } - .padding(.leading, 20) VStack(alignment: .leading, spacing: 2) { - Text(subscription.title) - .foregroundColor(.grey8) - .font(.system(size: 20, weight: .semibold, design: .rounded)) + Text(subscription.name) + .foregroundColor(.Foreground100) + .font(.system(size: 15, weight: .medium, design: .rounded)) Text(subscription.subtitle) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundColor(.Foreground150) + .font(.system(size: 14, weight: .regular, design: .rounded)) } Spacer() - Image("forward-shevron") - .foregroundColor(.grey8) - .padding(.trailing, 20) + if subscription.hasMessage { + VStack{ + Text(String(subscription.messagesCount)) + .foregroundColor(.Inverse100) + .font(.system(size: 13, weight: .medium).monospacedDigit()) + .padding(.horizontal, 8) + .padding(.vertical, 4) + }.background { + Capsule().foregroundColor(.blue100) + } + } + } + } + } + } + + private func discoverListRow(listing: ListingViewModel) -> some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + CacheAsyncImage(url: listing.imageUrl) { phase in + if let image = phase.image { + image + .resizable() + .frame(width: 48.0, height: 48.0) + .background(Color.grey8.opacity(0.1)) + .cornerRadius(30, corners: .allCorners) + } else { + Color.grey8.opacity(0.1) + .frame(width: 48.0, height: 48.0) + .cornerRadius(30, corners: .allCorners) + } + } + + Spacer() + + if let subscription = presenter.subscription(forListing: listing) { + AsyncButton("Subscribed") { + try await presenter.unsubscribe(subscription: subscription) + } + .buttonStyle(W3MButtonStyle(size: .m, variant: .accent, rightIcon: .Checkmark)) + .disabled(true) + } else { + AsyncButton("Subscribe") { + try await presenter.subscribe(listing: listing) + } + .buttonStyle(W3MButtonStyle(size: .m)) } } + + VStack(alignment: .leading, spacing: 2) { + Text(listing.title) + .font(.paragraph700) + .foregroundColor(.Foreground100) + + Text(listing.appDomain ?? .empty) + .foregroundColor(.Foreground200) + .font(.system(size: 12, weight: .medium, design: .rounded)) + } + + Text(listing.subtitle) + .foregroundColor(.Foreground150) + .font(.system(size: 14, weight: .regular, design: .rounded)) + .lineLimit(2) } + .padding(16.0) + .background( + RadialGradient(gradient: Gradient(colors: [.Blue100.opacity(0.1), .clear]), center: .topLeading, startRadius: 0, endRadius: 300) + ) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.grey95, lineWidth: 1) + ) + } +} + +private struct PreventCollapseView: View { + + private var mostlyClear = Color(UIColor(white: 0.0, alpha: 0.0005)) + + var body: some View { + Rectangle() + .fill(mostlyClear) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesInteractor.swift new file mode 100644 index 000000000..85ce91040 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesInteractor.swift @@ -0,0 +1,8 @@ +import WalletConnectNotify + +final class NotifyPreferencesInteractor { + + func updatePreferences(subscription: NotifySubscription, scope: Set) async throws { + try await Notify.instance.update(topic: subscription.topic, scope: scope) + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesModule.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesModule.swift new file mode 100644 index 000000000..d914a29c0 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesModule.swift @@ -0,0 +1,19 @@ +import SwiftUI +import WalletConnectNotify + +final class NotifyPreferencesModule { + + @discardableResult + static func create(app: Application, subscription: NotifySubscription) -> UIViewController { + let router = NotifyPreferencesRouter(app: app) + let interactor = NotifyPreferencesInteractor() + let presenter = NotifyPreferencesPresenter(subscription: subscription, interactor: interactor, router: router) + let view = NotifyPreferencesView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + router.viewController = viewController + + return viewController + } + +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift new file mode 100644 index 000000000..d2689effe --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift @@ -0,0 +1,58 @@ +import UIKit +import Combine +import WalletConnectNotify + +final class NotifyPreferencesPresenter: ObservableObject { + + private let subscription: NotifySubscription + private let interactor: NotifyPreferencesInteractor + private let router: NotifyPreferencesRouter + private var disposeBag = Set() + + var subscriptionViewModel: SubscriptionsViewModel { + return SubscriptionsViewModel(subscription: subscription, messages: []) + } + + var preferences: [String] { + return subscriptionViewModel.scope.keys.sorted() + } + + var isUpdateDisabled: Bool { + return update == subscription.scope + } + + @Published var update: SubscriptionScope = [:] + + init(subscription: NotifySubscription, interactor: NotifyPreferencesInteractor, router: NotifyPreferencesRouter) { + defer { setupInitialState() } + self.subscription = subscription + self.interactor = interactor + self.router = router + } + + @MainActor + func updateDidPress() async throws { + let scope = update + .filter { $0.value.enabled } + .map { $0.key } + + try await interactor.updatePreferences(subscription: subscription, scope: Set(scope)) + + router.dismiss() + } +} + +// MARK: SceneViewModel + +extension NotifyPreferencesPresenter: SceneViewModel { + +} + +// MARK: Privates + +private extension NotifyPreferencesPresenter { + + func setupInitialState() { + update = subscription.scope + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesRouter.swift similarity index 61% rename from Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesRouter.swift rename to Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesRouter.swift index 0dd264603..124db05f5 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesRouter.swift @@ -1,6 +1,6 @@ import UIKit -final class PushMessagesRouter { +final class NotifyPreferencesRouter { weak var viewController: UIViewController! @@ -9,4 +9,8 @@ final class PushMessagesRouter { init(app: Application) { self.app = app } + + func dismiss() { + viewController.dismiss() + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift new file mode 100644 index 000000000..327c2b4d6 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift @@ -0,0 +1,78 @@ +import SwiftUI +import AsyncButton +import WalletConnectNotify + +struct NotifyPreferencesView: View { + + @EnvironmentObject var viewModel: NotifyPreferencesPresenter + + var body: some View { + List { + VStack(spacing: 0) { + Text("Notification Preferences") + .font(.headline) + .foregroundColor(.primary) + .padding(.top, 16.0) + + Text("for \(viewModel.subscriptionViewModel.name)") + .font(.caption) + .foregroundColor(.secondary) + .padding(4.0) + } + .frame(maxWidth: .infinity) + .alignmentGuide(.listRowSeparatorLeading) { _ in -50 } + .listRowBackground(Color.clear) + + ForEach(Array(viewModel.preferences.enumerated()), id: \.offset) { i, preference in + if let value = viewModel.subscriptionViewModel.scope[preference] { + preferenceRow(title: preference, value: value) + .listRowSeparator(i == viewModel.preferences.count-1 ? .hidden : .visible) + .listRowBackground(Color.clear) + } + } + + AsyncButton { + try await viewModel.updateDidPress() + } label: { + Text("Update") + .frame(maxWidth: .infinity) + .frame(height: 44.0) + .foregroundColor(.white) + .font(.system(size: 16, weight: .semibold)) + .background(Color.blue100) + .cornerRadius(20) + } + .buttonStyle(.plain) + .disabled(viewModel.isUpdateDisabled) + .listRowBackground(Color.clear) + } + .listStyle(.plain) + } + + private func preferenceRow(title: String, value: ScopeValue) -> some View { + Toggle(isOn: .init(get: { + viewModel.update[title]?.enabled ?? value.enabled + }, set: { newValue in + viewModel.update[title] = ScopeValue(id: value.id, name: value.name, description: value.description, enabled: newValue) + })) { + VStack(alignment: .leading, spacing: 4) { + Text(value.name) + .foregroundColor(.primary) + .font(.system(size: 14, weight: .semibold)) + + Text(value.description) + .foregroundColor(.grey50) + .font(.system(size: 13)) + } + } + .padding(8.0) + } +} + +#if DEBUG +struct NotifyPreferencesView_Previews: PreviewProvider { + static var previews: some View { + NotifyPreferencesView() + } +} +#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift similarity index 92% rename from Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift rename to Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift index cfb200823..9e937154c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift @@ -1,7 +1,7 @@ import Foundation import WalletConnectNotify -struct PushMessageViewModel: Identifiable { +struct NotifyMessageViewModel: Identifiable { let pushMessageRecord: NotifyMessageRecord diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift deleted file mode 100644 index ec8b91c8f..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift +++ /dev/null @@ -1,71 +0,0 @@ -import UIKit -import Combine -import WalletConnectNotify - -final class PushMessagesPresenter: ObservableObject { - - private let interactor: PushMessagesInteractor - private let router: PushMessagesRouter - private var disposeBag = Set() - - @Published private var pushMessages: [NotifyMessageRecord] = [] - - var messages: [PushMessageViewModel] { - return pushMessages - .sorted { $0.publishedAt > $1.publishedAt } - .map { PushMessageViewModel(pushMessageRecord: $0) } - } - - init(interactor: PushMessagesInteractor, router: PushMessagesRouter) { - defer { setupInitialState() } - self.interactor = interactor - self.router = router - setUpMessagesRefresh() - } - - private func setUpMessagesRefresh() { - Timer.publish(every: 10.0, on: .main, in: .default) - .autoconnect() - .sink(receiveValue: { [weak self] _ in - guard let self = self else { return } - self.pushMessages = self.interactor.getPushMessages() - }).store(in: &disposeBag) - } - - - func deletePushMessage(at indexSet: IndexSet) { - if let index = indexSet.first { - interactor.deletePushMessage(id: pushMessages[index].id) - } - } -} - -// MARK: SceneViewModel - -extension PushMessagesPresenter: SceneViewModel { - var sceneTitle: String? { - return interactor.subscription.metadata.name - } - - var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { - return .always - } -} - -// MARK: Privates - -private extension PushMessagesPresenter { - - func setupInitialState() { - pushMessages = interactor.getPushMessages() - - interactor.messagesPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] messages in - guard let self = self else { return } - self.pushMessages = self.interactor.getPushMessages() - } - .store(in: &disposeBag) - } -} - diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift deleted file mode 100644 index 7f22c53f6..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift +++ /dev/null @@ -1,102 +0,0 @@ -import SwiftUI - -struct PushMessagesView: View { - - @EnvironmentObject var presenter: PushMessagesPresenter - - var body: some View { - ZStack { - Color.grey100 - .edgesIgnoringSafeArea(.all) - - VStack(alignment: .leading, spacing: 16) { - ZStack { - if presenter.messages.isEmpty { - VStack(spacing: 10) { - Image(systemName: "bell.badge.fill") - .resizable() - .frame(width: 32, height: 32) - .aspectRatio(contentMode: .fit) - .foregroundColor(.grey50) - - Text("Notifications from connected apps will appear here. To enable notifications, visit the app in your browser and look for a \(Image(systemName: "bell.fill")) notifications toggle \(Image(systemName: "switch.2"))") - .foregroundColor(.grey50) - .font(.system(size: 15, weight: .regular, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) - } - .padding(20) - } - - VStack { - if !presenter.messages.isEmpty { - List { - ForEach(presenter.messages, id: \.id) { pm in - notificationView(pushMessage: pm) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)) - } - .onDelete { indexSet in - presenter.deletePushMessage(at: indexSet) - } - } - .listStyle(PlainListStyle()) - } - } - } - } - .padding(.vertical, 20) - } - } - - - - private func notificationView(pushMessage: PushMessageViewModel) -> some View { - VStack { - HStack(spacing: 10) { - AsyncImage(url: URL(string: pushMessage.imageUrl)) { phase in - if let image = phase.image { - image - .resizable() - .frame(width: 60, height: 60) - .background(Color.black) - .cornerRadius(30, corners: .allCorners) - } else { - Color.black - .frame(width: 60, height: 60) - .cornerRadius(30, corners: .allCorners) - } - } - .padding(.leading, 20) - - - VStack(alignment: .leading, spacing: 2) { - Text(pushMessage.title) - .foregroundColor(.grey8) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - - HStack { - Text(pushMessage.subtitle) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .medium, design: .rounded)) - - Spacer() - - Text(pushMessage.publishedAt) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .medium, design: .rounded)) - } - } - .padding(.trailing, 20) - } - } - } -} - -#if DEBUG -struct PushMessagesView_Previews: PreviewProvider { - static var previews: some View { - PushMessagesView() - } -} -#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift similarity index 60% rename from Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift rename to Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift index b1953934e..9b0c84ff7 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift @@ -1,7 +1,7 @@ import WalletConnectNotify import Combine -final class PushMessagesInteractor { +final class SubscriptionInteractor { let subscription: NotifySubscription @@ -12,6 +12,10 @@ final class PushMessagesInteractor { var messagesPublisher: AnyPublisher<[NotifyMessageRecord], Never> { return Notify.instance.messagesPublisher(topic: subscription.topic) } + + var subscriptionPublisher: AnyPublisher<[NotifySubscription], Never> { + return Notify.instance.subscriptionsPublisher + } func getPushMessages() -> [NotifyMessageRecord] { return Notify.instance.getMessageHistory(topic: subscription.topic) @@ -20,4 +24,10 @@ final class PushMessagesInteractor { func deletePushMessage(id: String) { Notify.instance.deleteNotifyMessage(id: id) } + + func deleteSubscription(_ subscription: NotifySubscription) { + Task(priority: .high) { + try await Notify.instance.deleteSubscription(topic: subscription.topic) + } + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionModule.swift similarity index 52% rename from Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift rename to Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionModule.swift index 447c08ce3..2a84b6332 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionModule.swift @@ -1,14 +1,14 @@ import SwiftUI import WalletConnectNotify -final class PushMessagesModule { +final class SubscriptionModule { @discardableResult static func create(app: Application, subscription: NotifySubscription) -> UIViewController { - let router = PushMessagesRouter(app: app) - let interactor = PushMessagesInteractor(subscription: subscription) - let presenter = PushMessagesPresenter(interactor: interactor, router: router) - let view = PushMessagesView().environmentObject(presenter) + let router = SubscriptionRouter(app: app) + let interactor = SubscriptionInteractor(subscription: subscription) + let presenter = SubscriptionPresenter(subscription: subscription, interactor: interactor, router: router) + let view = SubscriptionView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) router.viewController = viewController diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift new file mode 100644 index 000000000..441c001e0 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift @@ -0,0 +1,103 @@ +import UIKit +import Combine +import WalletConnectNotify + +final class SubscriptionPresenter: ObservableObject { + + private var subscription: NotifySubscription + private let interactor: SubscriptionInteractor + private let router: SubscriptionRouter + private var disposeBag = Set() + + @Published private var pushMessages: [NotifyMessageRecord] = [] + + var subscriptionViewModel: SubscriptionsViewModel { + return SubscriptionsViewModel(subscription: subscription) + } + + var messages: [NotifyMessageViewModel] { + return pushMessages + .sorted { $0.publishedAt > $1.publishedAt } + .map { NotifyMessageViewModel(pushMessageRecord: $0) } + } + + init(subscription: NotifySubscription, interactor: SubscriptionInteractor, router: SubscriptionRouter) { + defer { setupInitialState() } + self.subscription = subscription + self.interactor = interactor + self.router = router + setUpMessagesRefresh() + } + + private func setUpMessagesRefresh() { + Timer.publish(every: 10.0, on: .main, in: .default) + .autoconnect() + .sink(receiveValue: { [weak self] _ in + guard let self = self else { return } + self.pushMessages = self.interactor.getPushMessages() + }).store(in: &disposeBag) + } + + func deletePushMessage(at indexSet: IndexSet) { + if let index = indexSet.first { + interactor.deletePushMessage(id: pushMessages[index].id) + } + } + + func unsubscribe() { + interactor.deleteSubscription(subscription) + router.dismiss() + } + + @objc func preferencesDidPress() { + router.presentPreferences(subscription: subscription) + } +} + +// MARK: SceneViewModel + +extension SubscriptionPresenter: SceneViewModel { + + var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { + return .never + } + + var navigationBarStyle: NavigationBarStyle { + return .clear + } + + var rightBarButtonItem: UIBarButtonItem? { + return UIBarButtonItem( + image: UIImage(systemName: "gearshape"), + style: .plain, + target: self, + action: #selector(preferencesDidPress) + ) + } +} + +// MARK: Privates + +private extension SubscriptionPresenter { + + func setupInitialState() { + pushMessages = interactor.getPushMessages() + + interactor.messagesPublisher + .receive(on: DispatchQueue.main) + .debounce(for: 1, scheduler: RunLoop.main) + .sink { [weak self] messages in + guard let self = self else { return } + self.pushMessages = self.interactor.getPushMessages() + } + .store(in: &disposeBag) + + interactor.subscriptionPublisher + .sink { [unowned self] subscriptions in + if let updated = subscriptions.first(where: { $0.topic == subscription.topic }) { + subscription = updated + } + }.store(in: &disposeBag) + } +} + diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionRouter.swift new file mode 100644 index 000000000..edcef5f8f --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionRouter.swift @@ -0,0 +1,24 @@ +import UIKit +import WalletConnectNotify + +final class SubscriptionRouter { + + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } + + func dismiss() { + viewController.pop() + } + + func presentPreferences(subscription: NotifySubscription) { + let controller = NotifyPreferencesModule.create(app: app, subscription: subscription) + controller.sheetPresentationController?.detents = [.custom(resolver: { _ in UIScreen.main.bounds.height * 2/3 })] + controller.sheetPresentationController?.prefersGrabberVisible = true + controller.present(from: viewController) + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift new file mode 100644 index 000000000..2a37713ef --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift @@ -0,0 +1,166 @@ +import SwiftUI + +struct SubscriptionView: View { + + @EnvironmentObject var presenter: SubscriptionPresenter + + var body: some View { + ZStack { + VStack { + RadialGradient(gradient: Gradient(colors: [.Blue100.opacity(0.1), .clear]), center: .topLeading, startRadius: 0, endRadius: 300) + .frame(height: 300) + Spacer() + } + + List { + headerView() + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + + if !presenter.messages.isEmpty { + ForEach(presenter.messages, id: \.id) { pm in + notificationView(pushMessage: pm) + .listRowSeparator(.visible) + .listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)) + .listRowBackground(Color.clear) + } + .onDelete { indexSet in + presenter.deletePushMessage(at: indexSet) + } + } else { + emptyStateView() + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + } + .listStyle(PlainListStyle()) + } + .ignoresSafeArea(.container) + .safeAreaInset(edge: .bottom) { Spacer().frame(height: 50) } + } + + private func notificationView(pushMessage: NotifyMessageViewModel) -> some View { + VStack(alignment: .center) { + HStack(spacing: 12) { + CacheAsyncImage(url: URL(string: pushMessage.imageUrl)) { phase in + if let image = phase.image { + image + .resizable() + .frame(width: 48, height: 48) + .background(Color.black) + .cornerRadius(10, corners: .allCorners) + } else { + Color.black + .frame(width: 48, height: 48) + .cornerRadius(10, corners: .allCorners) + } + } + .padding(.leading, 20) + + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(pushMessage.title) + .foregroundColor(.Foreground100) + .font(.system(size: 14, weight: .semibold)) + + Spacer() + + Text(pushMessage.publishedAt) + .foregroundColor(.Foreground250) + .font(.system(size: 11)) + } + + Text(pushMessage.subtitle) + .foregroundColor(.Foreground175) + .font(.system(size: 13)) + + } + .padding(.trailing, 20) + } + } + } + + func headerView() -> some View { + VStack(spacing: 0) { + CacheAsyncImage(url: presenter.subscriptionViewModel.imageUrl) { phase in + if let image = phase.image { + image + .resizable() + .frame(width: 64, height: 64) + } else { + Color.black + .frame(width: 64, height: 64) + } + } + .clipShape(Circle()) + .padding(.top, 56.0) + .padding(.bottom, 8.0) + + Text(presenter.subscriptionViewModel.name) + .font(.large700) + .foregroundColor(.Foreground100) + .padding(.bottom, 8.0) + + Text(presenter.subscriptionViewModel.domain) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.Foreground200) + .padding(.bottom, 16.0) + + Text(presenter.subscriptionViewModel.description) + .font(.system(size: 14, weight: .regular)) + .foregroundColor(.Foreground100) + .padding(.bottom, 16.0) + + Menu { + Button(role: .destructive, action: { + presenter.unsubscribe() + }) { + Label("Unsubscribe", systemImage: "x.circle") + } + } label: { + HStack(spacing: 16.0) { + Text("Subscribed") + .font(.subheadline) + .foregroundColor(.secondary) + + Image(systemName: "checkmark") + .foregroundColor(.secondary) + } + .padding(.horizontal, 16.0) + .padding(.vertical, 8.0) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.grey95, lineWidth: 1) + ) + } + .padding(.bottom, 20.0) + } + .frame(maxWidth: .infinity) + } + + func emptyStateView() -> some View { + VStack(spacing: 10) { + Image(systemName: "bell.badge.fill") + .resizable() + .frame(width: 32, height: 32) + .aspectRatio(contentMode: .fit) + .foregroundColor(.grey50) + + Text("Notifications from connected apps will appear here. To enable notifications, visit the app in your browser and look for a \(Image(systemName: "bell.fill")) notifications toggle \(Image(systemName: "switch.2"))") + .foregroundColor(.grey50) + .font(.system(size: 15, weight: .regular, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + } + .padding(20) + } +} + +#if DEBUG +struct PushMessagesView_Previews: PreviewProvider { + static var previews: some View { + SubscriptionView() + } +} +#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift index ad02dd7a4..be919a381 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift @@ -9,7 +9,7 @@ final class SessionProposalPresenter: ObservableObject { let importAccount: ImportAccount let sessionProposal: Session.Proposal - let verified: Bool? + let validationStatus: VerifyContext.ValidationStatus? @Published var showError = false @Published var errorMessage = "Error" @@ -28,7 +28,7 @@ final class SessionProposalPresenter: ObservableObject { self.router = router self.sessionProposal = proposal self.importAccount = importAccount - self.verified = (context?.validation == .valid) ? true : (context?.validation == .unknown ? nil : false) + self.validationStatus = context?.validation } @MainActor diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift index 8082ce1ee..cd595a158 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift @@ -22,22 +22,6 @@ struct SessionProposalView: View { Text(presenter.sessionProposal.proposer.name) .foregroundColor(.grey8) .font(.system(size: 22, weight: .bold, design: .rounded)) - - if let verified = presenter.verified { - if verified { - Image(systemName: "checkmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - } else { - Image(systemName: "xmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - } - } else { - Image(systemName: "exclamationmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .orange) - } } .padding(.top, 10) @@ -46,12 +30,40 @@ struct SessionProposalView: View { .foregroundColor(.grey8) .font(.system(size: 22, weight: .medium, design: .rounded)) - Text(presenter.sessionProposal.proposer.name) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - .multilineTextAlignment(.center) - .lineSpacing(4) + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.sessionProposal.proposer.url) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } .padding(.top, 8) + } else { + Text(presenter.sessionProposal.proposer.url) + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } + + switch presenter.validationStatus { + case .unknown: + verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) + + case .invalid: + verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) + + case .scam: + verifyBadgeView(imageName: "exclamationmark.shield.fill", title: "Security risk", color: .red) + + default: + EmptyView() + } Divider() .padding(.top, 12) @@ -80,7 +92,7 @@ struct SessionProposalView: View { .lineSpacing(4) .padding(.vertical, 12) } - + ForEach(optionalNamespaces.keys.sorted(), id: \.self) { chain in if let namespaces = optionalNamespaces[chain] { sessionProposalView(namespaces: namespaces) @@ -88,55 +100,37 @@ struct SessionProposalView: View { } } } - .frame(height: 250) - .padding(.top, 12) + .frame(height: 150) + .cornerRadius(20) + .padding(.vertical, 12) - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) - } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + switch presenter.validationStatus { + case .invalid: + verifyDescriptionView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .red) - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + case .unknown: + verifyDescriptionView(imageName: "exclamationmark.circle.fill", title: "Unknown domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .orange) + + case .scam: + verifyDescriptionView(imageName: "exclamationmark.shield.fill", title: "Security risk", description: "This website is flagged as unsafe by multiple security providers. Leave immediately to protect your assets.", color: .red) + + default: + EmptyView() + } + + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() + } + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) } .padding(20) .background(.ultraThinMaterial) @@ -151,7 +145,7 @@ struct SessionProposalView: View { } .edgesIgnoringSafeArea(.all) } - //private func sessionProposalView(chain: String) -> some View { + private func sessionProposalView(namespaces: ProposalNamespace) -> some View { VStack { VStack(alignment: .leading) { @@ -229,6 +223,100 @@ struct SessionProposalView: View { } .padding(.bottom, 15) } + + private func verifyBadgeView(imageName: String, title: String, color: Color) -> some View { + HStack(spacing: 5) { + Image(systemName: imageName) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(color) + + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + } + .padding(5) + .background(color.opacity(0.15)) + .cornerRadius(10) + .padding(.top, 8) + } + + private func verifyDescriptionView(imageName: String, title: String, description: String, color: Color) -> some View { + HStack(spacing: 15) { + Image(systemName: imageName) + .font(.system(size: 20, design: .rounded)) + .foregroundColor(color) + + VStack(alignment: .leading, spacing: 5) { + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + Text(description) + .foregroundColor(.grey8) + .font(.system(size: 14, weight: .medium, design: .rounded)) + } + } + .frame(maxWidth: .infinity) + .padding() + .background(color.opacity(0.15)) + .cornerRadius(20) + } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } } #if DEBUG diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift index b3dadc1f9..97816ea69 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift @@ -22,4 +22,8 @@ final class SessionRequestInteractor { response: .error(.init(code: 0, message: "")) ) } + + func getSession(topic: String) -> Session? { + return Web3Wallet.instance.getSessions().first(where: { $0.topic == topic }) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 99ee1cc7c..103002dd6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -9,10 +9,13 @@ final class SessionRequestPresenter: ObservableObject { private let importAccount: ImportAccount let sessionRequest: Request - let verified: Bool? + let session: Session? + let validationStatus: VerifyContext.ValidationStatus? var message: String { - return String(describing: sessionRequest.params.value) + let message = try? sessionRequest.params.get([String].self) + let decryptedMessage = message.map { String(data: Data(hex: $0.first ?? ""), encoding: .utf8) } + return (decryptedMessage ?? String(describing: sessionRequest.params.value)) ?? String(describing: sessionRequest.params.value) } @Published var showError = false @@ -31,8 +34,9 @@ final class SessionRequestPresenter: ObservableObject { self.interactor = interactor self.router = router self.sessionRequest = sessionRequest + self.session = interactor.getSession(topic: sessionRequest.topic) self.importAccount = importAccount - self.verified = (context?.validation == .valid) ? true : (context?.validation == .unknown ? nil : false) + self.validationStatus = context?.validation } @MainActor @@ -55,9 +59,7 @@ final class SessionRequestPresenter: ObservableObject { // MARK: - Private functions private extension SessionRequestPresenter { - func setupInitialState() { - - } + func setupInitialState() {} } // MARK: - SceneViewModel diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift index dc56b2e24..c8ce70396 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift @@ -17,79 +17,77 @@ struct SessionRequestView: View { .resizable() .scaledToFit() - HStack { - Text(presenter.sessionRequest.method) + Text(presenter.sessionRequest.method) + .foregroundColor(.grey8) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .padding(.top, 10) + + if case .valid = presenter.validationStatus { + HStack { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(.blue) + + Text(presenter.session?.peer.url ?? "") + .foregroundColor(.grey8) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .lineSpacing(4) + } + .padding(.top, 8) + } else { + Text(presenter.session?.peer.url ?? "") .foregroundColor(.grey8) - .font(.system(size: 22, weight: .bold, design: .rounded)) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(.top, 8) + } + + switch presenter.validationStatus { + case .unknown: + verifyBadgeView(imageName: "exclamationmark.circle.fill", title: "Cannot verify", color: .orange) - if let verified = presenter.verified { - if verified { - Image(systemName: "checkmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - } else { - Image(systemName: "xmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - } - } else { - Image(systemName: "exclamationmark.shield.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .orange) - } + case .invalid: + verifyBadgeView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", color: .red) + + case .scam: + verifyBadgeView(imageName: "exclamationmark.shield.fill", title: "Security risk", color: .red) + + default: + EmptyView() } - .padding(.top, 10) if presenter.message != "[:]" { authRequestView() } - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) - } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + switch presenter.validationStatus { + case .invalid: + verifyDescriptionView(imageName: "exclamationmark.triangle.fill", title: "Invalid domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .red) - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) + case .unknown: + verifyDescriptionView(imageName: "exclamationmark.circle.fill", title: "Unknown domain", description: "This domain cannot be verified. Check the request carefully before approving.", color: .orange) + + case .scam: + verifyDescriptionView(imageName: "exclamationmark.shield.fill", title: "Security risk", description: "This website is flagged as unsafe by multiple security providers. Leave immediately to protect your assets.", color: .red) + + default: + EmptyView() + } + + if case .scam = presenter.validationStatus { + VStack(spacing: 20) { + declineButton() + allowButton() + } + .padding(.top, 25) + } else { + HStack { + declineButton() + allowButton() } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + .padding(.top, 25) } - .padding(.top, 25) } .padding(20) .background(.ultraThinMaterial) @@ -123,10 +121,11 @@ struct SessionRequestView: View { Text(presenter.message) .foregroundColor(.grey50) .font(.system(size: 13, weight: .semibold, design: .rounded)) + .frame(maxWidth: .infinity, alignment: .leading) } .padding(.horizontal, 18) .padding(.vertical, 10) - .frame(height: 250) + .frame(height: 150) } .background(Color.whiteBackground) .cornerRadius(20, corners: .allCorners) @@ -137,7 +136,101 @@ struct SessionRequestView: View { .background(.thinMaterial) .cornerRadius(25, corners: .allCorners) } - .padding(.top, 30) + .padding(.vertical, 30) + } + + private func verifyBadgeView(imageName: String, title: String, color: Color) -> some View { + HStack(spacing: 5) { + Image(systemName: imageName) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundColor(color) + + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + } + .padding(5) + .background(color.opacity(0.15)) + .cornerRadius(10) + .padding(.top, 8) + } + + private func verifyDescriptionView(imageName: String, title: String, description: String, color: Color) -> some View { + HStack(spacing: 15) { + Image(systemName: imageName) + .font(.system(size: 20, design: .rounded)) + .foregroundColor(color) + + VStack(alignment: .leading, spacing: 5) { + Text(title) + .foregroundColor(color) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + + Text(description) + .foregroundColor(.grey8) + .font(.system(size: 14, weight: .medium, design: .rounded)) + } + } + .frame(maxWidth: .infinity) + .padding() + .background(color.opacity(0.15)) + .cornerRadius(20) + } + + private func declineButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onReject() + } + } label: { + Text("Decline") + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + LinearGradient( + gradient: Gradient(colors: [ + .foregroundNegative, + .lightForegroundNegative + ]), + startPoint: .top, endPoint: .bottom) + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) + } + + private func allowButton() -> some View { + Button { + Task(priority: .userInitiated) { try await + presenter.onApprove() + } + } label: { + Text(presenter.validationStatus == .scam ? "Proceed anyway" : "Allow") + .frame(maxWidth: .infinity) + .foregroundColor(presenter.validationStatus == .scam ? .grey50 : .white) + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .padding(.vertical, 11) + .background( + Group { + if presenter.validationStatus == .scam { + Color.clear + } else { + LinearGradient( + gradient: Gradient(colors: [ + .foregroundPositive, + .lightForegroundPositive + ]), + startPoint: .top, endPoint: .bottom + ) + } + } + ) + .cornerRadius(20) + } + .shadow(color: .white.opacity(0.25), radius: 8, y: 2) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift index 9b9e2779d..93592457e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift @@ -1,3 +1,9 @@ +import Foundation +import WalletConnectNotify + final class SettingsInteractor { + func notifyUnregister(account: Account) async throws { + try await Notify.instance.unregister(account: account) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 7a705a6bb..fb827251e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -38,10 +38,12 @@ final class SettingsPresenter: ObservableObject { return deviceToken } - func logoutPressed() { + func logoutPressed() async throws { + guard let account = accountStorage.importAccount?.account else { return } + try await interactor.notifyUnregister(account: account) accountStorage.importAccount = nil UserDefaults.standard.set(nil, forKey: "deviceToken") - router.presentWelcome() + await router.presentWelcome() } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift index 4069cd33d..7ef69186e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift @@ -10,7 +10,7 @@ final class SettingsRouter { self.app = app } - func presentWelcome() { + @MainActor func presentWelcome() async { WelcomeModule.create(app: app).present() } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 49578cf6e..137296793 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AsyncButton struct SettingsView: View { @@ -19,8 +20,8 @@ struct SettingsView: View { } Section { - Button { - viewModel.logoutPressed() + AsyncButton { + try await viewModel.logoutPressed() } label: { Text("Log out") .foregroundColor(.red) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index c8a5baee1..5a1bc3b85 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -120,12 +120,10 @@ extension WalletPresenter { } private func pairFromDapp() { - guard let uri = app.uri, - let walletConnectUri = WalletConnectURI(string: uri) - else { + guard let uri = app.uri else { return } - pair(uri: walletConnectUri) + pair(uri: uri) } private func removePairingIndicator() { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift deleted file mode 100644 index ad0633441..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift +++ /dev/null @@ -1,13 +0,0 @@ -import SwiftUI - -final class Web3InboxModule { - - @discardableResult - static func create(app: Application) -> UIViewController { - let router = Web3InboxRouter(app: app) - let viewController = Web3InboxViewController() - router.viewController = viewController - return viewController - } - -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift deleted file mode 100644 index d79c8b4ea..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift +++ /dev/null @@ -1,48 +0,0 @@ -import UIKit -import WebKit -import Web3Inbox - -final class Web3InboxViewController: UIViewController { - - private var webView: WKWebView? { - return view as? WKWebView - } - - init() { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - edgesForExtendedLayout = [] - navigationItem.title = "Web3Inbox SDK" - navigationItem.largeTitleDisplayMode = .never - view = Web3Inbox.instance.getWebView() - - let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refreshTapped)) - let getUrl = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(getUrlPressed)) - - navigationItem.rightBarButtonItems = [refresh, getUrl] - } - - @objc func refreshTapped() { - Web3Inbox.instance.reload() - } - - @objc func getUrlPressed(_ sender: UIBarItem) { - UIPasteboard.general.string = webView?.url?.absoluteString - - let alert = UIAlertController(title: "URL copied to clipboard", message: nil, preferredStyle: .alert) - let action = UIAlertAction(title: "OK", style: .cancel) - alert.addAction(action) - present(alert, animated: true) - } -} - - - diff --git a/NotifyTests.xctestplan b/NotifyTests.xctestplan index fc50c4a84..881dd5fda 100644 --- a/NotifyTests.xctestplan +++ b/NotifyTests.xctestplan @@ -15,6 +15,10 @@ "key" : "RELAY_HOST", "value" : "$(RELAY_HOST)" }, + { + "key" : "GM_DAPP_HOST", + "value" : "$(GM_DAPP_HOST)" + }, { "key" : "GM_DAPP_PROJECT_SECRET", "value" : "$(GM_DAPP_PROJECT_SECRET)" @@ -41,6 +45,11 @@ "testTargets" : [ { "selectedTests" : [ + "NotifyTests\/testNotifyServerSubscribeAndNotifies()", + "NotifyTests\/testNotifySubscriptionChanged()", + "NotifyTests\/testNotifyWatchSubscriptions()", + "NotifyTests\/testWalletCreatesAndUpdatesSubscription()", + "NotifyTests\/testWalletCreatesSubscription()", "PushTests\/testNotifyServerSubscribeAndNotifies()", "PushTests\/testWalletCreatesAndUpdatesSubscription()" ], diff --git a/Package.swift b/Package.swift index 8815d2a74..edc0d65fe 100644 --- a/Package.swift +++ b/Package.swift @@ -46,9 +46,6 @@ let package = Package( .library( name: "WalletConnectHistory", targets: ["WalletConnectHistory"]), - .library( - name: "Web3Inbox", - targets: ["Web3Inbox"]), .library( name: "WalletConnectModal", targets: ["WalletConnectModal"]), @@ -98,9 +95,6 @@ let package = Package( .target( name: "WalletConnectHistory", dependencies: ["HTTPClient", "WalletConnectRelay"]), - .target( - name: "Web3Inbox", - dependencies: ["WalletConnectChat", "WalletConnectNotify"]), .target( name: "WalletConnectSigner", dependencies: ["WalletConnectNetworking"]), diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index c2a7030c0..af26070ab 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -44,12 +44,12 @@ class WalletRequestSubscriber { Task(priority: .high) { let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let origin = try await verifyClient.verifyOrigin(assertionId: assertionId) - let verifyContext = verifyClient.createVerifyContext(origin: origin, domain: payload.request.payloadParams.domain) + let response = try await verifyClient.verifyOrigin(assertionId: assertionId) + let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: payload.request.payloadParams.domain, isScam: response.isScam) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.payloadParams.domain) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.payloadParams.domain, isScam: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) return diff --git a/Sources/Auth/Types/AuthPayload.swift b/Sources/Auth/Types/AuthPayload.swift index 23338434f..33ba49756 100644 --- a/Sources/Auth/Types/AuthPayload.swift +++ b/Sources/Auth/Types/AuthPayload.swift @@ -36,7 +36,7 @@ public struct AuthPayload: Codable, Equatable { throw Errors.invalidChainID } return CacaoPayload( - iss: DIDPKH(account: account).string, + iss: account.did, domain: domain, aud: aud, version: version, diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 7d0f8ed5a..13b66378b 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -89,10 +89,16 @@ public class ChatClient { @discardableResult public func register(account: Account, isPrivate: Bool = false, + domain: String, onSign: @escaping SigningCallback ) async throws -> String { - let publicKey = try await identityClient.register(account: account, onSign: onSign) - + let publicKey = try await identityClient.register( + account: account, + domain: domain, + statement: "statement", + resources: ["https://keys.walletconnect.com"], + onSign: onSign + ) if !syncRegisterService.isRegistered(account: account) { try await chatStorage.initializeHistory(account: account) try await syncRegisterService.register(account: account, onSign: onSign) @@ -109,9 +115,8 @@ public class ChatClient { /// Unregisters a blockchain account with previously registered identity key /// Must not unregister invite key but must stop listening for invites - /// - Parameter onSign: Callback for signing CAIP-122 message to verify blockchain account ownership - public func unregister(account: Account, onSign: @escaping SigningCallback) async throws { - try await identityClient.unregister(account: account, onSign: onSign) + public func unregister(account: Account) async throws { + try await identityClient.unregister(account: account) } /// Queries the keyserver with a blockchain account diff --git a/Sources/Chat/Types/Payloads/MessagePayload.swift b/Sources/Chat/Types/Payloads/MessagePayload.swift index 445612dc9..1452b2160 100644 --- a/Sources/Chat/Types/Payloads/MessagePayload.swift +++ b/Sources/Chat/Types/Payloads/MessagePayload.swift @@ -54,7 +54,7 @@ struct MessagePayload: JWTClaimsCodable { iat: defaultIatMilliseconds(), exp: expiry(days: 30), ksu: keyserver.absoluteString, - aud: DIDPKH(account: recipientAccount).string, + aud: recipientAccount.did, sub: message, act: Claims.action ) diff --git a/Sources/Chat/Types/Payloads/ReceiptPayload.swift b/Sources/Chat/Types/Payloads/ReceiptPayload.swift index 90362b305..304b4662a 100644 --- a/Sources/Chat/Types/Payloads/ReceiptPayload.swift +++ b/Sources/Chat/Types/Payloads/ReceiptPayload.swift @@ -52,7 +52,7 @@ struct ReceiptPayload: JWTClaimsCodable { exp: expiry(days: 30), ksu: keyserver.absoluteString, sub: messageHash, - aud: DIDPKH(account: senderAccount).string, + aud: senderAccount.did, act: Claims.action ) } diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift index 5168430a3..6ca9b78ab 100644 --- a/Sources/WalletConnectHistory/HistoryClientFactory.swift +++ b/Sources/WalletConnectHistory/HistoryClientFactory.swift @@ -4,15 +4,19 @@ class HistoryClientFactory { static func create() -> HistoryClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + let keyValueStorage = UserDefaults.standard + let logger = ConsoleLogger() return HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", - keychain: keychain + keyValueStorage: keyValueStorage, + keychain: keychain, + logger: logger ) } - static func create(historyUrl: String, relayUrl: String, keychain: KeychainStorageProtocol) -> HistoryClient { - let clientIdStorage = ClientIdStorage(keychain: keychain) + static func create(historyUrl: String, relayUrl: String, keyValueStorage: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryClient { + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychain, logger: logger) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) let historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage) diff --git a/Sources/WalletConnectIdentity/IdentityClient.swift b/Sources/WalletConnectIdentity/IdentityClient.swift index 57335fb79..a776adb09 100644 --- a/Sources/WalletConnectIdentity/IdentityClient.swift +++ b/Sources/WalletConnectIdentity/IdentityClient.swift @@ -22,8 +22,8 @@ public final class IdentityClient { self.logger = logger } - public func register(account: Account, onSign: SigningCallback) async throws -> String { - let pubKey = try await identityService.registerIdentity(account: account, onSign: onSign) + public func register(account: Account, domain: String, statement: String, resources: [String], onSign: SigningCallback) async throws -> String { + let pubKey = try await identityService.registerIdentity(account: account, domain: domain, statement: statement, resources: resources, onSign: onSign) logger.debug("Did register an account: \(account)") return pubKey } @@ -34,8 +34,8 @@ public final class IdentityClient { return inviteKey } - public func unregister(account: Account, onSign: SigningCallback) async throws { - try await identityService.unregister(account: account, onSign: onSign) + public func unregister(account: Account) async throws { + try await identityService.unregister(account: account) logger.debug("Did unregister an account: \(account)") } diff --git a/Sources/WalletConnectIdentity/IdentityService.swift b/Sources/WalletConnectIdentity/IdentityService.swift index 5f7fc0431..0a974c474 100644 --- a/Sources/WalletConnectIdentity/IdentityService.swift +++ b/Sources/WalletConnectIdentity/IdentityService.swift @@ -26,6 +26,9 @@ actor IdentityService { } func registerIdentity(account: Account, + domain: String, + statement: String, + resources: [String], onSign: SigningCallback ) async throws -> String { @@ -34,7 +37,8 @@ actor IdentityService { } let identityKey = SigningPrivateKey() - let cacao = try await makeCacao(DIDKey: identityKey.publicKey.did, account: account, onSign: onSign) + let audience = identityKey.publicKey.did + let cacao = try await makeCacao(account: account, domain: domain, statement: statement, resources: resources, audience: audience, onSign: onSign) try await networkService.registerIdentity(cacao: cacao) return try storage.saveIdentityKey(identityKey, for: account).publicKey.hexRepresentation @@ -54,7 +58,7 @@ actor IdentityService { return try storage.saveInviteKey(inviteKey, for: account) } - func unregister(account: Account, onSign: SigningCallback) async throws { + func unregister(account: Account) async throws { let identityKey = try storage.getIdentityKey(for: account) let identityPublicKey = DIDKey(rawData: identityKey.publicKey.rawRepresentation) let idAuth = try makeIDAuth(account: account, issuer: identityPublicKey, claims: UnregisterIdentityClaims.self) @@ -85,22 +89,26 @@ actor IdentityService { private extension IdentityService { - func makeCacao( - DIDKey: String, - account: Account, + func makeCacao(account: Account, + domain: String, + statement: String, + resources: [String], + audience: String, onSign: SigningCallback ) async throws -> Cacao { let cacaoHeader = CacaoHeader(t: "eip4361") let cacaoPayload = CacaoPayload( iss: account.did, - domain: keyserverURL.host!, - aud: getAudience(), + domain: domain, + aud: audience, version: getVersion(), nonce: getNonce(), iat: iatProvader.iat, - nbf: nil, exp: nil, statement: "statement", requestId: nil, - resources: [DIDKey] + nbf: nil, exp: nil, + statement: statement, + requestId: nil, + resources: resources ) let result = await onSign(try messageFormatter.formatMessage(from: cacaoPayload)) @@ -132,8 +140,4 @@ private extension IdentityService { private func getVersion() -> String { return "1" } - - private func getAudience() -> String { - return keyserverURL.absoluteString - } } diff --git a/Sources/WalletConnectJWT/JSONEncoder+JWT.swift b/Sources/WalletConnectJWT/JSONEncoder+JWT.swift new file mode 100644 index 000000000..7f73ad98c --- /dev/null +++ b/Sources/WalletConnectJWT/JSONEncoder+JWT.swift @@ -0,0 +1,11 @@ +import Foundation + +extension JSONEncoder { + + public static var jwt: JSONEncoder { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .withoutEscapingSlashes + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + return jsonEncoder + } +} diff --git a/Sources/WalletConnectJWT/JWT.swift b/Sources/WalletConnectJWT/JWT.swift index f1eab051c..d37728840 100644 --- a/Sources/WalletConnectJWT/JWT.swift +++ b/Sources/WalletConnectJWT/JWT.swift @@ -2,13 +2,21 @@ import Foundation struct JWT: Codable, Equatable { - var header: JWTHeader - var claims: JWTClaims - var signature: String? + let header: JWTHeader + let claims: JWTClaims + let signature: String + let string: String - init(header: JWTHeader = JWTHeader(), claims: JWTClaims) { - self.header = header + init(claims: JWTClaims, signer: JWTSigning, jsonEncoder: JSONEncoder = .jwt) throws { + self.header = JWTHeader(alg: signer.alg) self.claims = claims + + let headerString = try header.encode(jsonEncoder: jsonEncoder) + let claimsString = try claims.encode(jsonEncoder: jsonEncoder) + let signature = try signer.sign(header: headerString, claims: claimsString) + + self.signature = signature + self.string = [headerString, claimsString, signature].joined(separator: ".") } init(string: String) throws { @@ -19,19 +27,6 @@ struct JWT: Codable, Equatable { self.header = try JWTHeader.decode(from: components[0]) self.claims = try JWTClaims.decode(from: components[1]) self.signature = components[2] - } - - mutating func sign(using jwtSigner: JWTSigning) throws { - header.alg = jwtSigner.alg - let headerString = try header.encode() - let claimsString = try claims.encode() - self.signature = try jwtSigner.sign(header: headerString, claims: claimsString) - } - - func encoded() throws -> String { - guard let signature = signature else { throw JWTError.jwtNotSigned } - let headerString = try header.encode() - let claimsString = try claims.encode() - return [headerString, claimsString, signature].joined(separator: ".") + self.string = string } } diff --git a/Sources/WalletConnectJWT/JWTDecodable.swift b/Sources/WalletConnectJWT/JWTDecodable.swift index 2f96ae072..fa9d522f8 100644 --- a/Sources/WalletConnectJWT/JWTDecodable.swift +++ b/Sources/WalletConnectJWT/JWTDecodable.swift @@ -43,10 +43,8 @@ extension JWTClaimsCodable { public func signAndCreateWrapper(keyPair: SigningPrivateKey) throws -> Wrapper { let claims = try encode(iss: keyPair.publicKey.did) - var jwt = JWT(claims: claims) - try jwt.sign(using: EdDSASigner(keyPair)) - let jwtString = try jwt.encoded() - return Wrapper(jwtString: jwtString) + let jwt = try JWT(claims: claims, signer: EdDSASigner(keyPair)) + return Wrapper(jwtString: jwt.string) } public func defaultIat() -> UInt64 { diff --git a/Sources/WalletConnectJWT/JWTEncodable.swift b/Sources/WalletConnectJWT/JWTEncodable.swift index 04505fdb5..e612e47f8 100644 --- a/Sources/WalletConnectJWT/JWTEncodable.swift +++ b/Sources/WalletConnectJWT/JWTEncodable.swift @@ -1,17 +1,14 @@ import Foundation public protocol JWTEncodable: Codable, Equatable { - func encode() throws -> String + func encode(jsonEncoder: JSONEncoder) throws -> String static func decode(from string: String) throws -> Self } extension JWTEncodable { - public func encode() throws -> String { - let jsonEncoder = JSONEncoder() - jsonEncoder.outputFormatting = .withoutEscapingSlashes - jsonEncoder.dateEncodingStrategy = .secondsSince1970 + public func encode(jsonEncoder: JSONEncoder) throws -> String { let data = try jsonEncoder.encode(self) return JWTEncoder.base64urlEncodedString(data: data) } diff --git a/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift b/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift index a5b6d359d..53a271b14 100644 --- a/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift +++ b/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift @@ -7,16 +7,19 @@ public protocol KeyManagementServiceProtocol { func setPublicKey(publicKey: AgreementPublicKey, for topic: String) throws func setAgreementSecret(_ agreementSecret: AgreementKeys, topic: String) throws func setSymmetricKey(_ symmetricKey: SymmetricKey, for topic: String) throws + func setTopic(_ topic: String, for key: String) throws func getPrivateKey(for publicKey: AgreementPublicKey) throws -> AgreementPrivateKey? func getAgreementSecret(for topic: String) -> AgreementKeys? func getSymmetricKey(for topic: String) -> SymmetricKey? func getSymmetricKeyRepresentable(for topic: String) -> Data? func getPublicKey(for topic: String) -> AgreementPublicKey? + func getTopic(for key: String) -> String? func deletePrivateKey(for publicKey: String) func deleteAgreementSecret(for topic: String) func deleteSymmetricKey(for topic: String) func deletePublicKey(for topic: String) func deleteAll() throws + func deleteTopic(for key: String) func performKeyAgreement(selfPublicKey: AgreementPublicKey, peerPublicKey hexRepresentation: String) throws -> AgreementKeys } @@ -59,6 +62,18 @@ public class KeyManagementService: KeyManagementServiceProtocol { try keychain.add(agreementSecret, forKey: topic) } + public func setTopic(_ topic: String, for key: String) throws { + try keychain.add(topic, forKey: key) + } + + public func deleteTopic(for key: String) { + do { + try keychain.delete(key: key) + } catch { + print("Error deleting topic: \(error)") + } + } + public func getSymmetricKey(for topic: String) -> SymmetricKey? { do { return try keychain.read(key: topic) as SymmetricKey @@ -85,6 +100,14 @@ public class KeyManagementService: KeyManagementServiceProtocol { } } + public func getTopic(for key: String) -> String? { + do { + return try keychain.read(key: key) as String + } catch { + return nil + } + } + public func getAgreementSecret(for topic: String) -> AgreementKeys? { do { return try keychain.read(key: topic) as AgreementKeys diff --git a/Sources/WalletConnectKMS/Crypto/SymmetricKey.swift b/Sources/WalletConnectKMS/Crypto/SymmetricKey.swift index 7e2863f3c..0bfbe12a9 100644 --- a/Sources/WalletConnectKMS/Crypto/SymmetricKey.swift +++ b/Sources/WalletConnectKMS/Crypto/SymmetricKey.swift @@ -25,6 +25,10 @@ public struct SymmetricKey: Equatable { try self.init(rawRepresentation: data) } + public func derivedTopic() -> String { + rawRepresentation.sha256().toHexString() + } + } extension SymmetricKey: GenericPasswordConvertible { diff --git a/Sources/WalletConnectKMS/Keychain/KeychainError.swift b/Sources/WalletConnectKMS/Keychain/KeychainError.swift index 37974619c..78c16dcf7 100644 --- a/Sources/WalletConnectKMS/Keychain/KeychainError.swift +++ b/Sources/WalletConnectKMS/Keychain/KeychainError.swift @@ -1,13 +1,17 @@ import Foundation // TODO: Integrate with WalletConnectError -struct KeychainError: Error { +struct KeychainError: Error, LocalizedError { let status: OSStatus init(_ status: OSStatus) { self.status = status } + + var errorDescription: String? { + return "OSStatus: \(status), message: \(status.message)" + } } extension KeychainError: CustomStringConvertible { diff --git a/Sources/WalletConnectKMS/Serialiser/Serializer.swift b/Sources/WalletConnectKMS/Serialiser/Serializer.swift index 7a3dcf739..aed1406bd 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializer.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializer.swift @@ -107,11 +107,15 @@ public class Serializer: Serializing { } private func decode(sealbox: Data, symmetricKey: Data) throws -> (T, Data) { + var decryptedData = Data() + print(T.self) do { - let decryptedData = try codec.decode(sealbox: sealbox, symmetricKey: symmetricKey) + decryptedData = try codec.decode(sealbox: sealbox, symmetricKey: symmetricKey) let decodedType = try JSONDecoder().decode(T.self, from: decryptedData) return (decodedType, decryptedData) } catch { + let str = String(decoding: decryptedData, as: UTF8.self) + print(str) logger.error("Failed to decode with error: \(error)") throw error } diff --git a/Sources/WalletConnectModal/WalletConnectModal.swift b/Sources/WalletConnectModal/WalletConnectModal.swift index 7fe3c3674..87085fcf5 100644 --- a/Sources/WalletConnectModal/WalletConnectModal.swift +++ b/Sources/WalletConnectModal/WalletConnectModal.swift @@ -89,7 +89,11 @@ extension WalletConnectModal { let modal = WalletConnectModalSheetController() vc.present(modal, animated: true) } - + + public static func create() -> UIViewController { + return WalletConnectModalSheetController() + } + private static func topViewController(_ base: UIViewController? = nil) -> UIViewController? { let base = base ?? UIApplication diff --git a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift index 4470087d1..a5119544f 100644 --- a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift +++ b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift @@ -9,10 +9,10 @@ public struct NetworkingClientFactory { return NetworkingClientFactory.create(relayClient: relayClient, logger: logger, keychainStorage: keychainStorage, keyValueStorage: keyValueStorage) } - public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage) -> NetworkingInteractor { + public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage, kmsLogger: ConsoleLogging = ConsoleLogger(prefix: "🔐", loggingLevel: .off)) -> NetworkingInteractor { let kms = KeyManagementService(keychain: keychainStorage) - let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) + let serializer = Serializer(kms: kms, logger: kmsLogger) let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index cb48e6d47..f311cdc5b 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -2,6 +2,7 @@ import Foundation import Combine public class NetworkingInteractor: NetworkInteracting { + private var tasks = Task.DisposeBag() private var publishers = Set() private let relayClient: RelayClient private let serializer: Serializing @@ -91,14 +92,14 @@ public class NetworkingInteractor: NetworkInteracting { subscription: @escaping (RequestSubscriptionPayload) async throws -> Void ) { requestSubscription(on: protocolMethod) - .sink { (payload: RequestSubscriptionPayload) in + .sink { [unowned self] (payload: RequestSubscriptionPayload) in Task(priority: .high) { do { try await subscription(payload) } catch { errorHandler?.handle(error: error) } - } + }.store(in: &tasks) }.store(in: &publishers) } @@ -110,14 +111,14 @@ public class NetworkingInteractor: NetworkInteracting { subscription: @escaping (ResponseSubscriptionPayload) async throws -> Void ) { responseSubscription(on: protocolMethod) - .sink { (payload: ResponseSubscriptionPayload) in + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in Task(priority: .high) { do { try await subscription(payload) } catch { errorHandler?.handle(error: error) } - } + }.store(in: &tasks) }.store(in: &publishers) } diff --git a/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift b/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift deleted file mode 100644 index 2ae697d7d..000000000 --- a/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Combine - -class DeleteNotifySubscriptionSubscriber { - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let logger: ConsoleLogging - private let notifyStorage: NotifyStorage - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging, - notifyStorage: NotifyStorage - ) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.logger = logger - self.notifyStorage = notifyStorage - subscribeForDeleteSubscription() - } - - private func subscribeForDeleteSubscription() { - networkingInteractor.subscribeOnRequest( - protocolMethod: NotifyDeleteProtocolMethod(), - requestOfType: NotifyDeleteResponsePayload.Wrapper.self, - errorHandler: logger - ) { [unowned self] payload in - let (_, _) = try NotifyDeleteResponsePayload.decodeAndVerify(from: payload.request) - logger.debug("Peer deleted subscription") - } - } -} diff --git a/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift b/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift new file mode 100644 index 000000000..bd295ee30 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Array where Element: Hashable { + + func difference(from other: [Element]) -> [Element] { + let thisSet = Set(self) + let otherSet = Set(other) + return Array(thisSet.symmetricDifference(otherSet)) + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift b/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift new file mode 100644 index 000000000..7fe700216 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/Extensions/NotificationCenter.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine + +protocol NotificationPublishing { + func publisher(for name: NSNotification.Name) -> AnyPublisher +} + +extension NotificationCenter: NotificationPublishing { + func publisher(for name: NSNotification.Name) -> AnyPublisher { + return publisher(for: name, object: nil).eraseToAnyPublisher() + } +} + +#if DEBUG +class MockNotificationCenter: NotificationPublishing { + private let subject = PassthroughSubject() + + func publisher(for name: NSNotification.Name) -> AnyPublisher { + return subject.eraseToAnyPublisher() + } + + func post(name: NSNotification.Name) { + subject.send(Notification(name: name)) + } +} +#endif + diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift new file mode 100644 index 000000000..8a702fabd --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift @@ -0,0 +1,22 @@ +import Foundation + +final class NotifyAccountProvider { + enum Errors: Error { + case currentAccountNotFound + } + + private var currentAccount: Account? + + func setAccount(_ account: Account) { + self.currentAccount = account + } + + func logout() { + self.currentAccount = nil + } + + func getCurrentAccount() throws -> Account { + guard let currentAccount else { throw Errors.currentAccountNotFound } + return currentAccount + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index 96fa0374d..29a58e77f 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -5,15 +5,6 @@ public class NotifyClient { private var publishers = Set() - /// publishes new subscriptions - public var newSubscriptionPublisher: AnyPublisher { - return notifyStorage.newSubscriptionPublisher - } - - public var deleteSubscriptionPublisher: AnyPublisher { - return notifyStorage.deleteSubscriptionPublisher - } - public var subscriptionsPublisher: AnyPublisher<[NotifySubscription], Never> { return notifyStorage.subscriptionsPublisher } @@ -22,79 +13,96 @@ public class NotifyClient { return notifyMessageSubscriber.notifyMessagePublisher } - public var updateSubscriptionPublisher: AnyPublisher { - return notifyStorage.updateSubscriptionPublisher - } - public var logsPublisher: AnyPublisher { - logger.logsPublisher - .eraseToAnyPublisher() + return logger.logsPublisher } - private let deleteNotifySubscriptionService: DeleteNotifySubscriptionService + private let deleteNotifySubscriptionRequester: DeleteNotifySubscriptionRequester private let notifySubscribeRequester: NotifySubscribeRequester public let logger: ConsoleLogging private let pushClient: PushClient - private let identityClient: IdentityClient + private let identityService: NotifyIdentityService private let notifyStorage: NotifyStorage + private let notifyAccountProvider: NotifyAccountProvider private let notifyMessageSubscriber: NotifyMessageSubscriber private let resubscribeService: NotifyResubscribeService private let notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber - private let deleteNotifySubscriptionSubscriber: DeleteNotifySubscriptionSubscriber private let notifyUpdateRequester: NotifyUpdateRequester private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater + private let notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber + private let notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider + private let notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber + private let subscriptionWatcher: SubscriptionWatcher init(logger: ConsoleLogging, kms: KeyManagementServiceProtocol, - identityClient: IdentityClient, + identityService: NotifyIdentityService, pushClient: PushClient, notifyMessageSubscriber: NotifyMessageSubscriber, notifyStorage: NotifyStorage, - deleteNotifySubscriptionService: DeleteNotifySubscriptionService, + deleteNotifySubscriptionRequester: DeleteNotifySubscriptionRequester, resubscribeService: NotifyResubscribeService, notifySubscribeRequester: NotifySubscribeRequester, notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber, - deleteNotifySubscriptionSubscriber: DeleteNotifySubscriptionSubscriber, notifyUpdateRequester: NotifyUpdateRequester, notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, - subscriptionsAutoUpdater: SubscriptionsAutoUpdater + notifyAccountProvider: NotifyAccountProvider, + subscriptionsAutoUpdater: SubscriptionsAutoUpdater, + notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber, + notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider, + notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber, + subscriptionWatcher: SubscriptionWatcher ) { self.logger = logger self.pushClient = pushClient - self.identityClient = identityClient + self.identityService = identityService self.notifyMessageSubscriber = notifyMessageSubscriber self.notifyStorage = notifyStorage - self.deleteNotifySubscriptionService = deleteNotifySubscriptionService + self.deleteNotifySubscriptionRequester = deleteNotifySubscriptionRequester self.resubscribeService = resubscribeService self.notifySubscribeRequester = notifySubscribeRequester self.notifySubscribeResponseSubscriber = notifySubscribeResponseSubscriber - self.deleteNotifySubscriptionSubscriber = deleteNotifySubscriptionSubscriber self.notifyUpdateRequester = notifyUpdateRequester self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber + self.notifyAccountProvider = notifyAccountProvider self.subscriptionsAutoUpdater = subscriptionsAutoUpdater + self.notifyWatchSubscriptionsResponseSubscriber = notifyWatchSubscriptionsResponseSubscriber + self.notifyWatcherAgreementKeysProvider = notifyWatcherAgreementKeysProvider + self.notifySubscriptionsChangedRequestSubscriber = notifySubscriptionsChangedRequestSubscriber + self.subscriptionWatcher = subscriptionWatcher + } + + public func register(account: Account, domain: String, isLimited: Bool = false, onSign: @escaping SigningCallback) async throws { + try await identityService.register(account: account, domain: domain, isLimited: isLimited, onSign: onSign) + notifyAccountProvider.setAccount(account) + subscriptionWatcher.start() } - public func register(account: Account, onSign: @escaping SigningCallback) async throws { - _ = try await identityClient.register(account: account, onSign: onSign) + public func unregister(account: Account) async throws { + try await identityService.unregister(account: account) + notifyWatcherAgreementKeysProvider.removeAgreement(account: account) + notifyStorage.clearDatabase(account: account) + notifyAccountProvider.logout() + subscriptionWatcher.stop() } public func setLogging(level: LoggingLevel) { logger.setLogging(level: level) } - public func subscribe(metadata: AppMetadata, account: Account) async throws { - try await notifySubscribeRequester.subscribe(metadata: metadata, account: account) + public func subscribe(appDomain: String, account: Account) async throws { + try await notifySubscribeRequester.subscribe(appDomain: appDomain, account: account) } public func update(topic: String, scope: Set) async throws { try await notifyUpdateRequester.update(topic: topic, scope: scope) } - public func getActiveSubscriptions() -> [NotifySubscription] { - return notifyStorage.getSubscriptions() + public func getActiveSubscriptions(account: Account) -> [NotifySubscription] { + return notifyStorage.getSubscriptions(account: account) } public func getMessageHistory(topic: String) -> [NotifyMessageRecord] { @@ -102,7 +110,7 @@ public class NotifyClient { } public func deleteSubscription(topic: String) async throws { - try await deleteNotifySubscriptionService.delete(topic: topic) + try await deleteNotifySubscriptionRequester.delete(topic: topic) } public func deleteNotifyMessage(id: String) { @@ -114,7 +122,7 @@ public class NotifyClient { } public func isIdentityRegistered(account: Account) -> Bool { - return identityClient.isIdentityRegistered(account: account) + return identityService.isIdentityRegistered(account: account) } public func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift index 436cb724d..8fac80ce5 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -2,7 +2,7 @@ import Foundation public struct NotifyClientFactory { - public static func create(groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider) -> NotifyClient { + public static func create(projectId: String, groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String) -> NotifyClient { let logger = ConsoleLogger(prefix: "🔔",loggingLevel: .debug) let keyValueStorage = UserDefaults.standard let keyserverURL = URL(string: "https://keys.walletconnect.com")! @@ -10,6 +10,7 @@ public struct NotifyClientFactory { let groupKeychainService = GroupKeychainStorage(serviceIdentifier: groupIdentifier) return NotifyClientFactory.create( + projectId: projectId, keyserverURL: keyserverURL, logger: logger, keyValueStorage: keyValueStorage, @@ -18,11 +19,13 @@ public struct NotifyClientFactory { networkInteractor: networkInteractor, pairingRegisterer: pairingRegisterer, pushClient: pushClient, - crypto: crypto + crypto: crypto, + notifyHost: notifyHost ) } static func create( + projectId: String, keyserverURL: URL, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, @@ -31,48 +34,60 @@ public struct NotifyClientFactory { networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, - crypto: CryptoProvider + crypto: CryptoProvider, + notifyHost: String ) -> NotifyClient { let kms = KeyManagementService(keychain: keychainStorage) let subscriptionStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifySubscription) let messagesStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifyMessagesRecords) - let notifyStorage = NotifyStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore) + let notifyAccountProvider = NotifyAccountProvider() + let notifyStorage = NotifyStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore, accountProvider: notifyAccountProvider) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger) let notifyMessageSubscriber = NotifyMessageSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, notifyStorage: notifyStorage, crypto: crypto, logger: logger) - let webDidResolver = WebDidResolver() - let deleteNotifySubscriptionService = DeleteNotifySubscriptionService(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage) + let webDidResolver = NotifyWebDidResolver() + let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage) let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger) - let dappsMetadataStore = CodableStore(defaults: keyValueStorage, identifier: NotifyStorageIdntifiers.dappsMetadataStore) - let subscriptionScopeProvider = SubscriptionScopeProvider() + let notifyConfigProvider = NotifyConfigProvider(projectId: projectId) - let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, subscriptionScopeProvider: subscriptionScopeProvider, dappsMetadataStore: dappsMetadataStore) + let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider) - let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, dappsMetadataStore: dappsMetadataStore, subscriptionScopeProvider: subscriptionScopeProvider) + let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifyConfigProvider: notifyConfigProvider) - let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, subscriptionScopeProvider: subscriptionScopeProvider, logger: logger, notifyStorage: notifyStorage) + let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage) - let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, subscriptionScopeProvider: subscriptionScopeProvider, notifyStorage: notifyStorage) - - let deleteNotifySubscriptionSubscriber = DeleteNotifySubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage) + let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifyConfigProvider: notifyConfigProvider, notifyStorage: notifyStorage) let subscriptionsAutoUpdater = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, logger: logger, notifyStorage: notifyStorage) + let notifyWatcherAgreementKeysProvider = NotifyWatcherAgreementKeysProvider(kms: kms) + let notifyWatchSubscriptionsRequester = NotifyWatchSubscriptionsRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, webDidResolver: webDidResolver, notifyAccountProvider: notifyAccountProvider, notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, notifyHost: notifyHost) + let notifySubscriptionsBuilder = NotifySubscriptionsBuilder(notifyConfigProvider: notifyConfigProvider) + let notifyWatchSubscriptionsResponseSubscriber = NotifyWatchSubscriptionsResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage, groupKeychainStorage: groupKeychainStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) + let notifySubscriptionsChangedRequestSubscriber = NotifySubscriptionsChangedRequestSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, kms: kms, identityClient: identityClient, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) + let subscriptionWatcher = SubscriptionWatcher(notifyWatchSubscriptionsRequester: notifyWatchSubscriptionsRequester, logger: logger) + + let identityService = NotifyIdentityService(keyserverURL: keyserverURL, identityClient: identityClient, logger: logger) + return NotifyClient( logger: logger, kms: kms, - identityClient: identityClient, + identityService: identityService, pushClient: pushClient, notifyMessageSubscriber: notifyMessageSubscriber, notifyStorage: notifyStorage, - deleteNotifySubscriptionService: deleteNotifySubscriptionService, + deleteNotifySubscriptionRequester: deleteNotifySubscriptionRequester, resubscribeService: resubscribeService, notifySubscribeRequester: notifySubscribeRequester, notifySubscribeResponseSubscriber: notifySubscribeResponseSubscriber, - deleteNotifySubscriptionSubscriber: deleteNotifySubscriptionSubscriber, notifyUpdateRequester: notifyUpdateRequester, notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, - subscriptionsAutoUpdater: subscriptionsAutoUpdater + notifyAccountProvider: notifyAccountProvider, + subscriptionsAutoUpdater: subscriptionsAutoUpdater, + notifyWatchSubscriptionsResponseSubscriber: notifyWatchSubscriptionsResponseSubscriber, + notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, + notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber, + subscriptionWatcher: subscriptionWatcher ) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift new file mode 100644 index 000000000..b0e88b181 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift @@ -0,0 +1,35 @@ +import Foundation + +struct NotifyConfig: Codable { + struct NotificationType: Codable { + let id: String + let name: String + let description: String + } + struct ImageUrl: Codable { + let sm: String? + let md: String? + let lg: String? + } + let id: String + let name: String + let homepage: String + let description: String + let dapp_url: String + let image_url: ImageUrl? + let notificationTypes: [NotificationType] + + var appDomain: String { + return URL(string: dapp_url)?.host ?? dapp_url + } + + var metadata: AppMetadata { + return AppMetadata( + name: name, + description: + description, + url: appDomain, + icons: [image_url?.sm, image_url?.md, image_url?.lg].compactMap { $0 } + ) + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift new file mode 100644 index 000000000..362fce2fb --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift @@ -0,0 +1,33 @@ +import Foundation + +enum NotifyConfigAPI: HTTPService { + + var path: String { + return "/w3i/v1/notify-config" + } + + var method: HTTPMethod { + return .get + } + + var body: Data? { + return nil + } + + var queryParameters: [String : String]? { + switch self { + case .notifyDApps(let projectId, let appDomain): + return ["projectId": projectId, "appDomain": appDomain] + } + } + + var additionalHeaderFields: [String : String]? { + return nil + } + + var scheme: String { + return "https" + } + + case notifyDApps(projectId: String, appDomain: String) +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift new file mode 100644 index 000000000..8b196f592 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift @@ -0,0 +1,48 @@ +import Foundation + +actor NotifyConfigProvider { + + private let projectId: String + + private var cache: [String: NotifyConfig] = [:] + + init(projectId: String) { + self.projectId = projectId + } + + func resolveNotifyConfig(appDomain: String) async -> NotifyConfig { + if let config = cache[appDomain] { + return config + } + + do { + let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") + let request = NotifyConfigAPI.notifyDApps(projectId: projectId, appDomain: appDomain) + let response = try await httpClient.request(NotifyConfigResponse.self, at: request) + let config = response.data + cache[appDomain] = config + return config + } catch { + return emptyConfig(appDomain: appDomain) + } + } +} + +private extension NotifyConfigProvider { + + struct NotifyConfigResponse: Codable { + let data: NotifyConfig + } + + func emptyConfig(appDomain: String) -> NotifyConfig { + return NotifyConfig( + id: UUID().uuidString, + name: appDomain, + homepage: "https://\(appDomain)", + description: "", + dapp_url: "https://\(appDomain)", + image_url: nil, + notificationTypes: [] + ) + } +} diff --git a/Sources/WalletConnectNotify/Client/Common/NotifyDecryptionService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift similarity index 100% rename from Sources/WalletConnectNotify/Client/Common/NotifyDecryptionService.swift rename to Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift new file mode 100644 index 000000000..cf7b39101 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift @@ -0,0 +1,43 @@ +import Foundation + +final class NotifyIdentityService { + + private let keyserverURL: URL + private let identityClient: IdentityClient + private let logger: ConsoleLogging + + init(keyserverURL: URL, identityClient: IdentityClient, logger: ConsoleLogging) { + self.keyserverURL = keyserverURL + self.identityClient = identityClient + self.logger = logger + } + + public func register(account: Account, domain: String, isLimited: Bool, onSign: @escaping SigningCallback) async throws { + let statement = makeStatement(isLimited: isLimited) + _ = try await identityClient.register(account: account, + domain: domain, + statement: statement, + resources: [keyserverURL.absoluteString], + onSign: onSign) + } + + public func unregister(account: Account) async throws { + try await identityClient.unregister(account: account) + } + + func isIdentityRegistered(account: Account) -> Bool { + return identityClient.isIdentityRegistered(account: account) + } +} + +private extension NotifyIdentityService { + + func makeStatement(isLimited: Bool) -> String { + switch isLimited { + case true: + return "I further authorize this app to send and receive messages on my behalf for THIS domain using my WalletConnect identity. Read more at https://walletconnect.com/identity" + case false: + return "I further authorize this app to send and receive messages on my behalf for ALL domains using my WalletConnect identity. Read more at https://walletconnect.com/identity" + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift similarity index 89% rename from Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift rename to Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift index 80fbd12ec..18ee0048d 100644 --- a/Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift @@ -16,11 +16,11 @@ final class NotifyResubscribeService { setUpResubscription() } - func setUpResubscription() { + private func setUpResubscription() { networkInteractor.socketConnectionStatusPublisher .sink { [unowned self] status in guard status == .connected else { return } - let topics = notifyStorage.getSubscriptions().map{$0.topic} + let topics = notifyStorage.getAllSubscriptions().map { $0.topic } logger.debug("Resubscribing to notify subscription topics: \(topics)", properties: ["topics": topics.joined(separator: ", ")]) Task(priority: .high) { try await networkInteractor.batchSubscribe(topics: topics) diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift index ebc55c784..a7e2c92b8 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift @@ -2,10 +2,12 @@ import Foundation import Combine protocol NotifyStoring { - func getSubscriptions() -> [NotifySubscription] + func getAllSubscriptions() -> [NotifySubscription] + func getSubscriptions(account: Account) -> [NotifySubscription] func getSubscription(topic: String) -> NotifySubscription? func setSubscription(_ subscription: NotifySubscription) async throws func deleteSubscription(topic: String) async throws + func clearDatabase(account: Account) } final class NotifyStorage: NotifyStoring { @@ -21,6 +23,8 @@ final class NotifyStorage: NotifyStoring { private let subscriptionsSubject = PassthroughSubject<[NotifySubscription], Never>() private let messagesSubject = PassthroughSubject<[NotifyMessageRecord], Never>() + private let accountProvider: NotifyAccountProvider + var newSubscriptionPublisher: AnyPublisher { return newSubscriptionSubject.eraseToAnyPublisher() } @@ -37,23 +41,24 @@ final class NotifyStorage: NotifyStoring { return subscriptionsSubject.eraseToAnyPublisher() } - var messagesPublisher: AnyPublisher<[NotifyMessageRecord], Never> { - return messagesSubject.eraseToAnyPublisher() - } - - init(subscriptionStore: KeyedDatabase, messagesStore: KeyedDatabase) { + init(subscriptionStore: KeyedDatabase, messagesStore: KeyedDatabase, accountProvider: NotifyAccountProvider) { self.subscriptionStore = subscriptionStore self.messagesStore = messagesStore + self.accountProvider = accountProvider setupSubscriptions() } // MARK: Subscriptions - func getSubscriptions() -> [NotifySubscription] { + func getAllSubscriptions() -> [NotifySubscription] { return subscriptionStore.getAll() } + func getSubscriptions(account: Account) -> [NotifySubscription] { + return subscriptionStore.getAll(for: account.absoluteString) + } + func getSubscription(topic: String) -> NotifySubscription? { return subscriptionStore.getAll().first(where: { $0.topic == topic }) } @@ -63,6 +68,10 @@ final class NotifyStorage: NotifyStoring { newSubscriptionSubject.send(subscription) } + func replaceAllSubscriptions(_ subscriptions: [NotifySubscription], account: Account) { + subscriptionStore.replace(elements: subscriptions, for: account.absoluteString) + } + func deleteSubscription(topic: String) throws { guard let subscription = getSubscription(topic: topic) else { throw Errors.subscriptionNotFound @@ -71,6 +80,13 @@ final class NotifyStorage: NotifyStoring { deleteSubscriptionSubject.send(topic) } + func clearDatabase(account: Account) { + for subscription in getSubscriptions(account: account) { + deleteMessages(topic: subscription.topic) + } + subscriptionStore.deleteAll(for: account.absoluteString) + } + func updateSubscription(_ subscription: NotifySubscription, scope: [String: ScopeValue], expiry: UInt64) { let expiry = Date(timeIntervalSince1970: TimeInterval(expiry)) let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey) @@ -81,7 +97,7 @@ final class NotifyStorage: NotifyStoring { // MARK: Messages func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { - return messagesPublisher + return messagesSubject .map { $0.filter { $0.topic == topic } } .eraseToAnyPublisher() } @@ -117,7 +133,8 @@ private extension NotifyStorage { } subscriptionStore.onUpdate = { [unowned self] in - subscriptionsSubject.send(subscriptionStore.getAll()) + guard let account = try? accountProvider.getCurrentAccount() else { return } + subscriptionsSubject.send(getSubscriptions(account: account)) } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift new file mode 100644 index 000000000..d2c9cd582 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift @@ -0,0 +1,47 @@ +import Foundation + +class NotifySubscriptionsBuilder { + private let notifyConfigProvider: NotifyConfigProvider + + init(notifyConfigProvider: NotifyConfigProvider) { + self.notifyConfigProvider = notifyConfigProvider + } + + func buildSubscriptions(_ notifyServerSubscriptions: [NotifyServerSubscription]) async throws -> [NotifySubscription] { + var result = [NotifySubscription]() + + for subscription in notifyServerSubscriptions { + let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: subscription.appDomain) + + do { + let topic = try SymmetricKey(hex: subscription.symKey).derivedTopic() + let scope = try await buildScope(selectedScope: subscription.scope, availableScope: config.notificationTypes) + + result.append(NotifySubscription( + topic: topic, + account: subscription.account, + relay: RelayProtocolOptions(protocol: "irn", data: nil), + metadata: config.metadata, + scope: scope, + expiry: subscription.expiry, + symKey: subscription.symKey + )) + } catch { + continue + } + } + + return result + } + + private func buildScope(selectedScope: [String], availableScope: [NotifyConfig.NotificationType]) async throws -> [String: ScopeValue] { + return availableScope.reduce(into: [:]) { + $0[$1.id] = ScopeValue( + id: $1.id, + name: $1.name, + description: $1.description, + enabled: selectedScope.contains($1.id) + ) + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift new file mode 100644 index 000000000..5789f26c3 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift @@ -0,0 +1,54 @@ +import Foundation + +final class NotifyWatcherAgreementKeysProvider { + + private let kms: KeyManagementServiceProtocol + + init(kms: KeyManagementServiceProtocol) { + self.kms = kms + } + + func generateAgreementKeysIfNeeded(notifyServerPublicKey: AgreementPublicKey, account: Account) throws -> (responseTopic: String, selfPubKeyY: Data) { + + let keyYStorageKey = storageKey(account: account) + + if + let responseTopic = kms.getTopic(for: keyYStorageKey), + let agreement = kms.getAgreementSecret(for: responseTopic), + let recoveredAgreement = try? kms.performKeyAgreement( + selfPublicKey: agreement.publicKey, + peerPublicKey: notifyServerPublicKey.hexRepresentation + ), agreement == recoveredAgreement + { + return (responseTopic: responseTopic, selfPubKeyY: agreement.publicKey.rawRepresentation) + } + else { + let selfPubKeyY = try kms.createX25519KeyPair() + let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() + + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: notifyServerPublicKey.hexRepresentation) + + try kms.setSymmetricKey(agreementKeys.sharedKey, for: watchSubscriptionsTopic) + let responseTopic = agreementKeys.derivedTopic() + + try kms.setAgreementSecret(agreementKeys, topic: responseTopic) + + // save for later under dapp's account + pub key + try kms.setTopic(responseTopic, for: keyYStorageKey) + + return (responseTopic: responseTopic, selfPubKeyY: selfPubKeyY.rawRepresentation) + } + } + + func removeAgreement(account: Account) { + let keyYStorageKey = storageKey(account: account) + kms.deleteTopic(for: keyYStorageKey) + } +} + +private extension NotifyWatcherAgreementKeysProvider { + + func storageKey(account: Account) -> String { + return "watchSubscriptionResponseTopic_\(account.absoluteString)" + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift new file mode 100644 index 000000000..5312e28bd --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift @@ -0,0 +1,44 @@ +import Foundation + +final class NotifyWebDidResolver { + + private static var subscribeKey = "wc-notify-subscribe-key" + private static var authenticationKey = "wc-notify-authentication-key" + + func resolveAgreementKey(domain: String) async throws -> AgreementPublicKey { + let didDoc = try await resolveDidDoc(domainUrl: domain) + let subscribeKeyPath = "\(didDoc.id)#\(Self.subscribeKey)" + guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == subscribeKeyPath }) else { throw Errors.noVerificationMethodForKey } + guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve} + let pubKeyBase64Url = verificationMethod.publicKeyJwk.x + return try AgreementPublicKey(base64url: pubKeyBase64Url) + } + + // TODO - Add cache for diddocs + + func resolveAuthenticationKey(domain: String) async throws -> Data { + let didDoc = try await resolveDidDoc(domainUrl: domain) + let authenticationKeyPath = "\(didDoc.id)#\(Self.authenticationKey)" + guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == authenticationKeyPath }) else { throw Errors.noVerificationMethodForKey } + guard verificationMethod.publicKeyJwk.crv == .Ed25519 else { throw Errors.unsupportedCurve} + let pubKeyBase64Url = verificationMethod.publicKeyJwk.x + guard let raw = Data(base64url: pubKeyBase64Url) else { throw Errors.invalidBase64urlString } + return raw + } +} + +private extension NotifyWebDidResolver { + + enum Errors: Error { + case invalidUrl + case invalidBase64urlString + case noVerificationMethodForKey + case unsupportedCurve + } + + func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc { + guard let didDocUrl = URL(string: "https://\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl } + let (data, _) = try await URLSession.shared.data(from: didDocUrl) + return try JSONDecoder().decode(WebDidDoc.self, from: data) + } +} diff --git a/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift similarity index 76% rename from Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift rename to Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift index fc4a361fd..ea3281849 100644 --- a/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift @@ -1,13 +1,13 @@ import Foundation -class DeleteNotifySubscriptionService { +class DeleteNotifySubscriptionRequester { enum Errors: Error { case notifySubscriptionNotFound } private let keyserver: URL private let networkingInteractor: NetworkInteracting private let identityClient: IdentityClient - private let webDidResolver: WebDidResolver + private let webDidResolver: NotifyWebDidResolver private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private let notifyStorage: NotifyStorage @@ -16,7 +16,7 @@ class DeleteNotifySubscriptionService { keyserver: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, - webDidResolver: WebDidResolver, + webDidResolver: NotifyWebDidResolver, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, notifyStorage: NotifyStorage @@ -37,12 +37,12 @@ class DeleteNotifySubscriptionService { else { throw Errors.notifySubscriptionNotFound} let protocolMethod = NotifyDeleteProtocolMethod() - let dappPubKey = try await webDidResolver.resolvePublicKey(dappUrl: subscription.metadata.url) + let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url) let wrapper = try createJWTWrapper( - dappPubKey: DIDKey(rawData: dappPubKey.rawRepresentation), + dappPubKey: DIDKey(rawData: dappAuthenticationKey), reason: NotifyDeleteParams.userDisconnected.message, - app: subscription.metadata.url, + app: DIDWeb(host: subscription.metadata.url), account: subscription.account ) @@ -58,12 +58,16 @@ class DeleteNotifySubscriptionService { kms.deleteSymmetricKey(for: topic) } + + + + } -private extension DeleteNotifySubscriptionService { +private extension DeleteNotifySubscriptionRequester { - func createJWTWrapper(dappPubKey: DIDKey, reason: String, app: String, account: Account) throws -> NotifyDeletePayload.Wrapper { - let jwtPayload = NotifyDeletePayload(keyserver: keyserver, dappPubKey: dappPubKey, reason: reason, app: app) + func createJWTWrapper(dappPubKey: DIDKey, reason: String, app: DIDWeb, account: Account) throws -> NotifyDeletePayload.Wrapper { + let jwtPayload = NotifyDeletePayload(account: account, keyserver: keyserver, dappPubKey: dappPubKey, app: app) return try identityClient.signAndCreateWrapper( payload: jwtPayload, account: account diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift index 5763c4b05..2ebd5125f 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift @@ -33,17 +33,18 @@ class NotifyMessageSubscriber { logger.debug("Received Notify Message on topic: \(payload.topic)", properties: ["topic": payload.topic]) let (messagePayload, claims) = try NotifyMessagePayload.decodeAndVerify(from: payload.request) + logger.debug("Decoded Notify Message: \(payload.topic)", properties: ["topic": payload.topic, "messageBody": messagePayload.message.body, "messageTitle": messagePayload.message.title, "publishedAt": payload.publishedAt.description, "id": payload.id.string]) - let dappPubKey = try DIDKey(did: claims.iss) - let messageData = try JSONEncoder().encode(messagePayload.message) + let dappPubKey = try DIDKey(did: claims.iss) let record = NotifyMessageRecord(id: payload.id.string, topic: payload.topic, message: messagePayload.message, publishedAt: payload.publishedAt) notifyStorage.setMessage(record) notifyMessagePublisherSubject.send(record) let receiptPayload = NotifyMessageReceiptPayload( - keyserver: keyserver, dappPubKey: dappPubKey, - messageHash: crypto.keccak256(messageData).toHexString(), + account: messagePayload.account, + keyserver: keyserver, + dappPubKey: dappPubKey, app: messagePayload.app ) diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift new file mode 100644 index 000000000..95d92a0a1 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift @@ -0,0 +1,105 @@ +import Foundation +import Combine + +class NotifySubscriptionsChangedRequestSubscriber { + private let keyserver: URL + private let networkingInteractor: NetworkInteracting + private let identityClient: IdentityClient + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let groupKeychainStorage: KeychainStorageProtocol + private let notifyStorage: NotifyStorage + private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + + init( + keyserver: URL, + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + identityClient: IdentityClient, + logger: ConsoleLogging, + groupKeychainStorage: KeychainStorageProtocol, + notifyStorage: NotifyStorage, + notifySubscriptionsBuilder: NotifySubscriptionsBuilder + ) { + self.keyserver = keyserver + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.identityClient = identityClient + self.groupKeychainStorage = groupKeychainStorage + self.notifyStorage = notifyStorage + self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + subscribeForNofifyChangedRequests() + } + + + private func subscribeForNofifyChangedRequests() { + networkingInteractor.subscribeOnRequest( + protocolMethod: NotifySubscriptionsChangedProtocolMethod(), + requestOfType: NotifySubscriptionsChangedRequestPayload.Wrapper.self, + errorHandler: logger) { [unowned self] payload in + logger.debug("Received Subscriptions Changed Request") + + let (jwtPayload, _) = try NotifySubscriptionsChangedRequestPayload.decodeAndVerify(from: payload.request) + let account = jwtPayload.account + + // TODO: varify signature with notify server diddoc authentication key + + let oldSubscriptions = notifyStorage.getSubscriptions(account: account) + let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions) + + try Task.checkCancellation() + + let subscriptions = oldSubscriptions.difference(from: newSubscriptions) + + logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") + + guard subscriptions.count > 0 else { return } + + notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) + + for subscription in newSubscriptions { + let symKey = try SymmetricKey(hex: subscription.symKey) + try groupKeychainStorage.add(symKey, forKey: subscription.topic) + try kms.setSymmetricKey(symKey, for: subscription.topic) + } + + let topics = newSubscriptions.map { $0.topic } + + try await networkingInteractor.batchSubscribe(topics: topics) + + try Task.checkCancellation() + + var logProperties = ["rpcId": payload.id.string] + for (index, subscription) in newSubscriptions.enumerated() { + let key = "subscription_\(index + 1)" + logProperties[key] = subscription.topic + } + + logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties) + + try await respond(topic: payload.topic, account: jwtPayload.account, rpcId: payload.id, notifyServerAuthenticationKey: jwtPayload.notifyServerAuthenticationKey) + } + } + + private func respond(topic: String, account: Account, rpcId: RPCID, notifyServerAuthenticationKey: DIDKey) async throws { + + let receiptPayload = NotifySubscriptionsChangedResponsePayload(account: account, keyserver: keyserver, notifyServerAuthenticationKey: notifyServerAuthenticationKey) + + let wrapper = try identityClient.signAndCreateWrapper( + payload: receiptPayload, + account: account + ) + + let response = RPCResponse(id: rpcId, result: wrapper) + try await networkingInteractor.respond( + topic: topic, + response: response, + protocolMethod: NotifySubscriptionsChangedProtocolMethod() + ) + + let logProperties = ["rpcId": rpcId.string] + logger.debug("Responded for Subscriptions Changed Request", properties: logProperties) + } + +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift index 8f4c8a103..7ddb67379 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift @@ -10,19 +10,19 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { } private let keyserverURL: URL - private let webDidResolver: WebDidResolver + private let webDidResolver: NotifyWebDidResolver private let identityClient: IdentityClient private let networkingInteractor: NetworkInteracting - private let subscriptionScopeProvider: SubscriptionScopeProvider + private let notifyConfigProvider: NotifyConfigProvider private let logger: ConsoleLogging private let notifyStorage: NotifyStorage init( keyserverURL: URL, - webDidResolver: WebDidResolver, + webDidResolver: NotifyWebDidResolver, identityClient: IdentityClient, networkingInteractor: NetworkInteracting, - subscriptionScopeProvider: SubscriptionScopeProvider, + notifyConfigProvider: NotifyConfigProvider, logger: ConsoleLogging, notifyStorage: NotifyStorage ) { @@ -30,7 +30,7 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { self.webDidResolver = webDidResolver self.identityClient = identityClient self.networkingInteractor = networkingInteractor - self.subscriptionScopeProvider = subscriptionScopeProvider + self.notifyConfigProvider = notifyConfigProvider self.logger = logger self.notifyStorage = notifyStorage } @@ -40,12 +40,12 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { guard let subscription = notifyStorage.getSubscription(topic: topic) else { throw Errors.noSubscriptionForGivenTopic } - let dappPubKey = try await webDidResolver.resolvePublicKey(dappUrl: subscription.metadata.url) + let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url) let request = try createJWTRequest( - dappPubKey: DIDKey(rawData: dappPubKey.rawRepresentation), + dappPubKey: DIDKey(rawData: dappAuthenticationKey), subscriptionAccount: subscription.account, - dappUrl: subscription.metadata.url, scope: scope + appDomain: subscription.metadata.url, scope: scope ) let protocolMethod = NotifyUpdateProtocolMethod() @@ -53,10 +53,11 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) } - private func createJWTRequest(dappPubKey: DIDKey, subscriptionAccount: Account, dappUrl: String, scope: Set) throws -> RPCRequest { + private func createJWTRequest(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String, scope: Set) throws -> RPCRequest { let protocolMethod = NotifyUpdateProtocolMethod().method let scopeClaim = scope.joined(separator: " ") - let jwtPayload = NotifyUpdatePayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scopeClaim) + let app = DIDWeb(host: appDomain) + let jwtPayload = NotifyUpdatePayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, app: app, scope: scopeClaim) let wrapper = try identityClient.signAndCreateWrapper( payload: jwtPayload, account: subscriptionAccount diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift index 2b489618b..d9b2bafbd 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift @@ -6,17 +6,17 @@ class NotifyUpdateResponseSubscriber { private var publishers = [AnyCancellable]() private let logger: ConsoleLogging private let notifyStorage: NotifyStorage - private let subscriptionScopeProvider: SubscriptionScopeProvider + private let nofityConfigProvider: NotifyConfigProvider init(networkingInteractor: NetworkInteracting, logger: ConsoleLogging, - subscriptionScopeProvider: SubscriptionScopeProvider, + notifyConfigProvider: NotifyConfigProvider, notifyStorage: NotifyStorage ) { self.networkingInteractor = networkingInteractor self.logger = logger self.notifyStorage = notifyStorage - self.subscriptionScopeProvider = subscriptionScopeProvider + self.nofityConfigProvider = notifyConfigProvider subscribeForUpdateResponse() } @@ -36,31 +36,10 @@ private extension NotifyUpdateResponseSubscriber { responseOfType: NotifyUpdateResponsePayload.Wrapper.self, errorHandler: logger ) { [unowned self] payload in - logger.debug("Received Notify Update response") - - let subscriptionTopic = payload.topic - - let (requestPayload, requestClaims) = try NotifyUpdatePayload.decodeAndVerify(from: payload.request) - let (_, _) = try NotifyUpdateResponsePayload.decodeAndVerify(from: payload.response) - - let scope = try await buildScope(selected: requestPayload.scope, dappUrl: requestPayload.dappUrl) - - guard let oldSubscription = notifyStorage.getSubscription(topic: subscriptionTopic) else { - logger.debug("NotifyUpdateResponseSubscriber Subscription does not exist") - return - } - notifyStorage.updateSubscription(oldSubscription, scope: scope, expiry: requestClaims.exp) + let _ = try NotifyUpdateResponsePayload.decodeAndVerify(from: payload.response) - logger.debug("Updated Subscription") - } - } - - func buildScope(selected: String, dappUrl: String) async throws -> [String: ScopeValue] { - let selectedScope = selected.components(separatedBy: " ") - let availableScope = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) - return availableScope.reduce(into: [:]) { - $0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name)) + logger.debug("Received Notify Update response") } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift new file mode 100644 index 000000000..72702c588 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift @@ -0,0 +1,85 @@ +import Foundation +import Combine + +protocol NotifyWatchSubscriptionsRequesting { + func watchSubscriptions() async throws +} + +class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { + + private let keyserverURL: URL + private let identityClient: IdentityClient + private let networkingInteractor: NetworkInteracting + private let logger: ConsoleLogging + private let webDidResolver: NotifyWebDidResolver + private let notifyAccountProvider: NotifyAccountProvider + private let notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider + private let notifyHost: String + private var publishers = Set() + + init(keyserverURL: URL, + networkingInteractor: NetworkInteracting, + identityClient: IdentityClient, + logger: ConsoleLogging, + webDidResolver: NotifyWebDidResolver, + notifyAccountProvider: NotifyAccountProvider, + notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider, + notifyHost: String + ) { + self.keyserverURL = keyserverURL + self.identityClient = identityClient + self.networkingInteractor = networkingInteractor + self.logger = logger + self.webDidResolver = webDidResolver + self.notifyAccountProvider = notifyAccountProvider + self.notifyWatcherAgreementKeysProvider = notifyWatcherAgreementKeysProvider + self.notifyHost = notifyHost + } + + func watchSubscriptions() async throws { + let account = try notifyAccountProvider.getCurrentAccount() + + logger.debug("Watching subscriptions") + + let notifyServerPublicKey = try await webDidResolver.resolveAgreementKey(domain: notifyHost) + let notifyServerAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: notifyHost) + let notifyServerAuthenticationDidKey = DIDKey(rawData: notifyServerAuthenticationKey) + let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() + + let (responseTopic, selfPubKeyY) = try notifyWatcherAgreementKeysProvider.generateAgreementKeysIfNeeded(notifyServerPublicKey: notifyServerPublicKey, account: account) + + logger.debug("setting symm key for response topic \(responseTopic)") + + let protocolMethod = NotifyWatchSubscriptionsProtocolMethod() + + let watchSubscriptionsAuthWrapper = try await createJWTWrapper( + notifyServerAuthenticationDidKey: notifyServerAuthenticationDidKey, + subscriptionAccount: account) + + let request = RPCRequest(method: protocolMethod.method, params: watchSubscriptionsAuthWrapper) + + logger.debug("Subscribing to response topic: \(responseTopic)") + + try await networkingInteractor.subscribe(topic: responseTopic) + + try await networkingInteractor.request(request, topic: watchSubscriptionsTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY)) + } + + private func createJWTWrapper(notifyServerAuthenticationDidKey: DIDKey, subscriptionAccount: Account) async throws -> NotifyWatchSubscriptionsPayload.Wrapper { + let jwtPayload = NotifyWatchSubscriptionsPayload(notifyServerAuthenticationKey: notifyServerAuthenticationDidKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount) + return try identityClient.signAndCreateWrapper( + payload: jwtPayload, + account: subscriptionAccount + ) + } +} + +#if DEBUG +class MockNotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { + var onWatchSubscriptions: (() -> Void)? + + func watchSubscriptions() async throws { + onWatchSubscriptions?() + } +} +#endif diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift new file mode 100644 index 000000000..298932dec --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift @@ -0,0 +1,76 @@ +import Foundation +import Combine + +class NotifyWatchSubscriptionsResponseSubscriber { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let notifyStorage: NotifyStorage + private let groupKeychainStorage: KeychainStorageProtocol + private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + notifyStorage: NotifyStorage, + groupKeychainStorage: KeychainStorageProtocol, + notifySubscriptionsBuilder: NotifySubscriptionsBuilder + ) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.notifyStorage = notifyStorage + self.groupKeychainStorage = groupKeychainStorage + self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + subscribeForWatchSubscriptionsResponse() + } + + + private func subscribeForWatchSubscriptionsResponse() { + networkingInteractor.subscribeOnResponse( + protocolMethod: NotifyWatchSubscriptionsProtocolMethod(), + requestOfType: NotifyWatchSubscriptionsPayload.Wrapper.self, + responseOfType: NotifyWatchSubscriptionsResponsePayload.Wrapper.self, + errorHandler: logger) { [unowned self] payload in + logger.debug("Received Notify Watch Subscriptions response") + + let (responsePayload, _) = try NotifyWatchSubscriptionsResponsePayload.decodeAndVerify(from: payload.response) + let (watchSubscriptionPayloadRequest, _) = try NotifyWatchSubscriptionsPayload.decodeAndVerify(from: payload.request) + + let account = watchSubscriptionPayloadRequest.subscriptionAccount + // TODO: varify signature with notify server diddoc authentication key + + let oldSubscriptions = notifyStorage.getSubscriptions(account: account) + let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) + + try Task.checkCancellation() + + let subscriptions = oldSubscriptions.difference(from: newSubscriptions) + + logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") + + guard subscriptions.count > 0 else { return } + // TODO: unsubscribe for oldSubscriptions topics that are not included in new subscriptions + notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) + + for subscription in newSubscriptions { + let symKey = try SymmetricKey(hex: subscription.symKey) + try groupKeychainStorage.add(symKey, forKey: subscription.topic) + try kms.setSymmetricKey(symKey, for: subscription.topic) + } + + try await networkingInteractor.batchSubscribe(topics: newSubscriptions.map { $0.topic }) + + try Task.checkCancellation() + + var logProperties = [String: String]() + for (index, subscription) in newSubscriptions.enumerated() { + let key = "subscription_\(index + 1)" + logProperties[key] = subscription.topic + } + + logger.debug("Updated Subscriptions with Watch Subscriptions Update, number of subscriptions: \(newSubscriptions.count)", properties: logProperties) + } + } + +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift index b6f6b7470..ad5c0cee0 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift @@ -12,18 +12,16 @@ class NotifySubscribeRequester { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementService private let logger: ConsoleLogging - private let webDidResolver: WebDidResolver - private let dappsMetadataStore: CodableStore - private let subscriptionScopeProvider: SubscriptionScopeProvider + private let webDidResolver: NotifyWebDidResolver + private let notifyConfigProvider: NotifyConfigProvider init(keyserverURL: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, logger: ConsoleLogging, kms: KeyManagementService, - webDidResolver: WebDidResolver, - subscriptionScopeProvider: SubscriptionScopeProvider, - dappsMetadataStore: CodableStore + webDidResolver: NotifyWebDidResolver, + notifyConfigProvider: NotifyConfigProvider ) { self.keyserverURL = keyserverURL self.identityClient = identityClient @@ -31,24 +29,21 @@ class NotifySubscribeRequester { self.logger = logger self.kms = kms self.webDidResolver = webDidResolver - self.subscriptionScopeProvider = subscriptionScopeProvider - self.dappsMetadataStore = dappsMetadataStore + self.notifyConfigProvider = notifyConfigProvider } - @discardableResult func subscribe(metadata: AppMetadata, account: Account) async throws -> NotifySubscriptionPayload.Wrapper { + @discardableResult func subscribe(appDomain: String, account: Account) async throws -> NotifySubscriptionPayload.Wrapper { - let dappUrl = metadata.url + logger.debug("Subscribing for Notify, dappUrl: \(appDomain)") - logger.debug("Subscribing for Notify") + let config = try await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) - let peerPublicKey = try await webDidResolver.resolvePublicKey(dappUrl: metadata.url) + let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: appDomain) let subscribeTopic = peerPublicKey.rawRepresentation.sha256().toHexString() let keysY = try generateAgreementKeys(peerPublicKey: peerPublicKey) let responseTopic = keysY.derivedTopic() - - dappsMetadataStore.set(metadata, forKey: responseTopic) try kms.setSymmetricKey(keysY.sharedKey, for: subscribeTopic) try kms.setAgreementSecret(keysY, topic: responseTopic) @@ -60,11 +55,11 @@ class NotifySubscribeRequester { let subscriptionAuthWrapper = try await createJWTWrapper( dappPubKey: DIDKey(did: peerPublicKey.did), subscriptionAccount: account, - dappUrl: dappUrl + appDomain: appDomain ) let request = RPCRequest(method: protocolMethod.method, params: subscriptionAuthWrapper) - logger.debug("NotifySubscribeRequester: subscribing to response topic: \(responseTopic)") + logger.debug("Subscribing to response topic: \(responseTopic)") try await networkingInteractor.subscribe(topic: responseTopic) @@ -79,10 +74,16 @@ class NotifySubscribeRequester { return keys } - private func createJWTWrapper(dappPubKey: DIDKey, subscriptionAccount: Account, dappUrl: String) async throws -> NotifySubscriptionPayload.Wrapper { - let types = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) - let scope = types.map{$0.name}.joined(separator: " ") - let jwtPayload = NotifySubscriptionPayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scope) + private func createJWTWrapper(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String) async throws -> NotifySubscriptionPayload.Wrapper { + let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) + let app = DIDWeb(host: appDomain) + let jwtPayload = NotifySubscriptionPayload( + dappPubKey: dappPubKey, + keyserver: keyserverURL, + subscriptionAccount: subscriptionAccount, + app: app, + scope: config.notificationTypes.map { $0.id }.joined(separator: " ") + ) return try identityClient.signAndCreateWrapper( payload: jwtPayload, account: subscriptionAccount diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift index 3642d3420..d8aa56a39 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift @@ -12,24 +12,21 @@ class NotifySubscribeResponseSubscriber { private let logger: ConsoleLogging private let notifyStorage: NotifyStorage private let groupKeychainStorage: KeychainStorageProtocol - private let dappsMetadataStore: CodableStore - private let subscriptionScopeProvider: SubscriptionScopeProvider + private let notifyConfigProvider: NotifyConfigProvider init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, groupKeychainStorage: KeychainStorageProtocol, notifyStorage: NotifyStorage, - dappsMetadataStore: CodableStore, - subscriptionScopeProvider: SubscriptionScopeProvider + notifyConfigProvider: NotifyConfigProvider ) { self.networkingInteractor = networkingInteractor self.kms = kms self.logger = logger self.groupKeychainStorage = groupKeychainStorage self.notifyStorage = notifyStorage - self.dappsMetadataStore = dappsMetadataStore - self.subscriptionScopeProvider = subscriptionScopeProvider + self.notifyConfigProvider = notifyConfigProvider subscribeForSubscriptionResponse() } @@ -40,62 +37,13 @@ class NotifySubscribeResponseSubscriber { responseOfType: NotifySubscriptionResponsePayload.Wrapper.self, errorHandler: logger ) { [unowned self] payload in - logger.debug("Received Notify Subscribe response") + logger.debug("Received Notify Subscribe response") - guard - let (responsePayload, _) = try? NotifySubscriptionResponsePayload.decodeAndVerify(from: payload.response) - else { fatalError() /* TODO: Handle error */ } + let _ = try NotifySubscriptionResponsePayload.decodeAndVerify(from: payload.response) - guard let responseKeys = kms.getAgreementSecret(for: payload.topic) else { - logger.debug("No symmetric key for topic \(payload.topic)") - throw Errors.couldNotCreateSubscription - } + logger.debug("NotifySubscribeResponseSubscriber: unsubscribing from response topic: \(payload.topic)") - // get keypair Y - let pubKeyY = responseKeys.publicKey - let peerPubKeyZ = responsePayload.publicKey.hexString - - var account: Account! - var metadata: AppMetadata! - var notifySubscriptionTopic: String! - var subscribedTypes: Set! - var agreementKeysP: AgreementKeys! - let (subscriptionPayload, claims) = try NotifySubscriptionPayload.decodeAndVerify(from: payload.request) - let subscribedScope = subscriptionPayload.scope - .components(separatedBy: " ") - do { - // generate symm key P - agreementKeysP = try kms.performKeyAgreement(selfPublicKey: pubKeyY, peerPublicKey: peerPubKeyZ) - notifySubscriptionTopic = agreementKeysP.derivedTopic() - try kms.setAgreementSecret(agreementKeysP, topic: notifySubscriptionTopic) - try groupKeychainStorage.add(agreementKeysP, forKey: notifySubscriptionTopic) - account = try Account(DIDPKHString: claims.sub) - metadata = try dappsMetadataStore.get(key: payload.topic) - let availableTypes = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: metadata!.url) - subscribedTypes = availableTypes.filter{subscribedScope.contains($0.name)} - logger.debug("NotifySubscribeResponseSubscriber: subscribing notify subscription topic: \(notifySubscriptionTopic!)") - try await networkingInteractor.subscribe(topic: notifySubscriptionTopic) - } catch { - logger.debug("NotifySubscribeResponseSubscriber: error: \(error)") - throw Errors.couldNotCreateSubscription - } - - guard let metadata = metadata else { - logger.debug("NotifySubscribeResponseSubscriber: no metadata for topic: \(notifySubscriptionTopic!)") - throw Errors.couldNotCreateSubscription - } - dappsMetadataStore.delete(forKey: payload.topic) - let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) - let scope: [String: ScopeValue] = subscribedTypes.reduce(into: [:]) { $0[$1.name] = ScopeValue(description: $1.description, enabled: true) } - let notifySubscription = NotifySubscription(topic: notifySubscriptionTopic, account: account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: metadata, scope: scope, expiry: expiry, symKey: agreementKeysP.sharedKey.hexRepresentation) - - notifyStorage.setSubscription(notifySubscription) - - logger.debug("NotifySubscribeResponseSubscriber: unsubscribing response topic: \(payload.topic)") - networkingInteractor.unsubscribe(topic: payload.topic) - } + networkingInteractor.unsubscribe(topic: payload.topic) + } } - - // TODO: handle error response - } diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift deleted file mode 100644 index a362dd4e6..000000000 --- a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift +++ /dev/null @@ -1,22 +0,0 @@ - -import Foundation - -class SubscriptionScopeProvider { - enum Errors: Error { - case invalidUrl - } - - private var cache = [String: Set]() - - func getSubscriptionScope(dappUrl: String) async throws -> Set { - if let availableScope = cache[dappUrl] { - return availableScope - } - guard let scopeUrl = URL(string: "\(dappUrl)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: scopeUrl) - let config = try JSONDecoder().decode(NotificationConfig.self, from: data) - let availableScope = Set(config.types) - cache[dappUrl] = availableScope - return availableScope - } -} diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift new file mode 100644 index 000000000..00ae7bebc --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift @@ -0,0 +1,84 @@ +import Foundation +import Combine +#if os(iOS) +import UIKit +#endif + +class SubscriptionWatcher { + + private var timerCancellable: AnyCancellable? + private var appLifecycleCancellable: AnyCancellable? + private var notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting + private let logger: ConsoleLogging + private let backgroundQueue = DispatchQueue(label: "com.walletconnect.subscriptionWatcher", qos: .background) + private let notificationCenter: NotificationPublishing + private var watchSubscriptionsWorkItem: DispatchWorkItem? + + var timerInterval: TimeInterval = 5 * 60 + var debounceInterval: TimeInterval = 0.5 + var onSetupTimer: (() -> Void)? + + init(notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting, + logger: ConsoleLogging, + notificationCenter: NotificationPublishing = NotificationCenter.default) { + self.notifyWatchSubscriptionsRequester = notifyWatchSubscriptionsRequester + self.logger = logger + self.notificationCenter = notificationCenter + } + + deinit { stop() } + + func start() { + setupTimer() + watchAppLifecycle() + watchSubscriptions() + } + + func stop() { + timerCancellable?.cancel() + appLifecycleCancellable?.cancel() + watchSubscriptionsWorkItem?.cancel() + } +} + +internal extension SubscriptionWatcher { + + func watchSubscriptions() { + watchSubscriptionsWorkItem?.cancel() + + let workItem = DispatchWorkItem { [weak self] in + self?.logger.debug("Will watch subscriptions") + Task(priority: .background) { [weak self] in try await self?.notifyWatchSubscriptionsRequester.watchSubscriptions() } + } + + watchSubscriptionsWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + debounceInterval, execute: workItem) + } + + func watchAppLifecycle() { +#if os(iOS) + appLifecycleCancellable = notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.logger.debug("Will setup Subscription Watcher after app entered foreground") + self?.setupTimer() + self?.backgroundQueue.async { + self?.watchSubscriptions() + } + } +#endif + } + + func setupTimer() { + onSetupTimer?() + logger.debug("Setting up Subscription Watcher timer") + timerCancellable?.cancel() + timerCancellable = Timer.publish(every: timerInterval, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.backgroundQueue.async { + self?.watchSubscriptions() + } + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift index 4eff66e89..5aca863e3 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift @@ -16,7 +16,7 @@ class SubscriptionsAutoUpdater { } private func updateSubscriptionsIfNeeded() { - for subscription in notifyStorage.getSubscriptions() { + for subscription in notifyStorage.getAllSubscriptions() { if shouldUpdate(subscription: subscription) { let scope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) let topic = subscription.topic diff --git a/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift b/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift deleted file mode 100644 index 2f27c2693..000000000 --- a/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -final class WebDidResolver { - - func resolvePublicKey(dappUrl: String) async throws -> AgreementPublicKey { - let didDoc = try await resolveDidDoc(domainUrl: dappUrl) - guard let keyAgreement = didDoc.keyAgreement.first else { throw Errors.didDocDoesNotContainKeyAgreement } - guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == keyAgreement }) else { throw Errors.noVerificationMethodForKey } - guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve} - let pubKeyBase64Url = verificationMethod.publicKeyJwk.x - return try AgreementPublicKey(base64url: pubKeyBase64Url) - } -} - -private extension WebDidResolver { - - enum Errors: Error { - case invalidUrl - case didDocDoesNotContainKeyAgreement - case noVerificationMethodForKey - case unsupportedCurve - } - - func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc { - guard let didDocUrl = URL(string: "\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: didDocUrl) - return try JSONDecoder().decode(WebDidDoc.self, from: data) - } -} diff --git a/Sources/WalletConnectNotify/NotifyConfig.swift b/Sources/WalletConnectNotify/Notify+Config.swift similarity index 86% rename from Sources/WalletConnectNotify/NotifyConfig.swift rename to Sources/WalletConnectNotify/Notify+Config.swift index 977f024f3..c7e699c03 100644 --- a/Sources/WalletConnectNotify/NotifyConfig.swift +++ b/Sources/WalletConnectNotify/Notify+Config.swift @@ -6,5 +6,6 @@ extension Notify { let groupIdentifier: String let environment: APNSEnvironment let crypto: CryptoProvider + let notifyHost: String } } diff --git a/Sources/WalletConnectNotify/Notify.swift b/Sources/WalletConnectNotify/Notify.swift index 9b258d89f..039c139d6 100644 --- a/Sources/WalletConnectNotify/Notify.swift +++ b/Sources/WalletConnectNotify/Notify.swift @@ -7,11 +7,13 @@ public class Notify { } Push.configure(pushHost: config.pushHost, environment: config.environment) return NotifyClientFactory.create( + projectId: Networking.projectId, groupIdentifier: config.groupIdentifier, networkInteractor: Networking.interactor, pairingRegisterer: Pair.registerer, pushClient: Push.instance, - crypto: config.crypto + crypto: config.crypto, + notifyHost: config.notifyHost ) }() @@ -20,8 +22,8 @@ public class Notify { private init() { } /// Wallet's configuration method - static public func configure(pushHost: String = "echo.walletconnect.com", groupIdentifier: String, environment: APNSEnvironment, crypto: CryptoProvider) { - Notify.config = Notify.Config(pushHost: pushHost, groupIdentifier: groupIdentifier, environment: environment, crypto: crypto) + static public func configure(pushHost: String = "echo.walletconnect.com", groupIdentifier: String, environment: APNSEnvironment, crypto: CryptoProvider, notifyHost: String = "notify.walletconnect.com") { + Notify.config = Notify.Config(pushHost: pushHost, groupIdentifier: groupIdentifier, environment: environment, crypto: crypto, notifyHost: notifyHost) } } diff --git a/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift index fb1b21c53..b68272b25 100644 --- a/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift +++ b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift @@ -4,6 +4,5 @@ enum NotifyStorageIdntifiers { static let notifySubscription = "com.walletconnect.notify.notifySubscription" static let notifyMessagesRecords = "com.walletconnect.sdk.notifyMessagesRecords" - static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore" static let coldStartStore = "com.walletconnect.sdk.coldStartStore" } diff --git a/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscriptionsChangedRequest.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscriptionsChangedRequest.swift new file mode 100644 index 000000000..6f5d28e22 --- /dev/null +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscriptionsChangedRequest.swift @@ -0,0 +1,9 @@ +import Foundation + +struct NotifySubscriptionsChangedProtocolMethod: ProtocolMethod { + let method: String = "wc_notifySubscriptionsChanged" + + let requestConfig: RelayConfig = RelayConfig(tag: 4012, prompt: false, ttl: 300) + + let responseConfig: RelayConfig = RelayConfig(tag: 4013, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectNotify/ProtocolMethods/NotifyWatchSubscriptionsProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifyWatchSubscriptionsProtocolMethod.swift new file mode 100644 index 000000000..a1a4db3a2 --- /dev/null +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifyWatchSubscriptionsProtocolMethod.swift @@ -0,0 +1,9 @@ +import Foundation + +struct NotifyWatchSubscriptionsProtocolMethod: ProtocolMethod { + let method: String = "wc_notifyWatchSubscriptions" + + let requestConfig: RelayConfig = RelayConfig(tag: 4010, prompt: false, ttl: 300) + + let responseConfig: RelayConfig = RelayConfig(tag: 4011, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectNotify/Types/NotifyError.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyError.swift similarity index 63% rename from Sources/WalletConnectNotify/Types/NotifyError.swift rename to Sources/WalletConnectNotify/Types/DataStructures/NotifyError.swift index 31d567893..d7aeeaf8f 100644 --- a/Sources/WalletConnectNotify/Types/NotifyError.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifyError.swift @@ -1,8 +1,6 @@ import Foundation public enum NotifyError: Codable, Equatable, Error { - case userRejeted - case userHasExistingSubscription case methodUnsupported case registerSignatureRejected } @@ -11,10 +9,6 @@ extension NotifyError: Reason { init?(code: Int) { switch code { - case Self.userRejeted.code: - self = .userRejeted - case Self.userHasExistingSubscription.code: - self = .userHasExistingSubscription case Self.methodUnsupported.code: self = .methodUnsupported case Self.registerSignatureRejected.code: @@ -27,10 +21,6 @@ extension NotifyError: Reason { switch self { case .methodUnsupported: return 10001 - case .userRejeted: - return 5000 - case .userHasExistingSubscription: - return 6001 case .registerSignatureRejected: return 1501 } @@ -40,10 +30,6 @@ extension NotifyError: Reason { switch self { case .methodUnsupported: return "Method Unsupported" - case .userRejeted: - return "Notify request rejected" - case .userHasExistingSubscription: - return "User Has Existing Subscription" case .registerSignatureRejected: return "Register signature rejected" } diff --git a/Sources/WalletConnectNotify/Types/NotifyMessage.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift similarity index 100% rename from Sources/WalletConnectNotify/Types/NotifyMessage.swift rename to Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift diff --git a/Sources/WalletConnectNotify/Types/NotifyRequest.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyRequest.swift similarity index 100% rename from Sources/WalletConnectNotify/Types/NotifyRequest.swift rename to Sources/WalletConnectNotify/Types/DataStructures/NotifyRequest.swift diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift new file mode 100644 index 000000000..56ebbd352 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift @@ -0,0 +1,9 @@ +import Foundation + +struct NotifyServerSubscription: Codable, Equatable { + let appDomain: String + let account: Account + let scope: [String] + let symKey: String + let expiry: Date +} diff --git a/Sources/WalletConnectNotify/Types/NotifySubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift similarity index 57% rename from Sources/WalletConnectNotify/Types/NotifySubscription.swift rename to Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift index b44189c80..cc47fce34 100644 --- a/Sources/WalletConnectNotify/Types/NotifySubscription.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift @@ -15,6 +15,15 @@ public struct NotifySubscription: DatabaseObject { } public struct ScopeValue: Codable, Equatable { - let description: String - let enabled: Bool + public let id: String + public let name: String + public let description: String + public let enabled: Bool + + public init(id: String, name: String, description: String, enabled: Bool) { + self.id = id + self.name = name + self.description = description + self.enabled = enabled + } } diff --git a/Sources/WalletConnectNotify/Types/NotifySubscriptionResult.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscriptionResult.swift similarity index 100% rename from Sources/WalletConnectNotify/Types/NotifySubscriptionResult.swift rename to Sources/WalletConnectNotify/Types/DataStructures/NotifySubscriptionResult.swift diff --git a/Sources/WalletConnectNotify/Types/WebDidDoc.swift b/Sources/WalletConnectNotify/Types/DataStructures/WebDidDoc.swift similarity index 86% rename from Sources/WalletConnectNotify/Types/WebDidDoc.swift rename to Sources/WalletConnectNotify/Types/DataStructures/WebDidDoc.swift index adf0c17f9..a31333b0c 100644 --- a/Sources/WalletConnectNotify/Types/WebDidDoc.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/WebDidDoc.swift @@ -5,12 +5,10 @@ struct WebDidDoc: Codable { let context: [String] let id: String let verificationMethod: [VerificationMethod] - let authentication: [String]? - let keyAgreement: [String] enum CodingKeys: String, CodingKey { case context = "@context" - case id, verificationMethod, authentication, keyAgreement + case id, verificationMethod } } extension WebDidDoc { diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeletePayload.swift similarity index 81% rename from Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeletePayload.swift index 62aa74204..5b6898d6f 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeletePayload.swift @@ -16,7 +16,7 @@ struct NotifyDeletePayload: JWTClaimsCodable { let iss: String /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. let aud: String - /// Reason for deleting the subscription + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp's domain url let app: String @@ -38,28 +38,28 @@ struct NotifyDeletePayload: JWTClaimsCodable { } } + let account: Account let keyserver: URL let dappPubKey: DIDKey - let reason: String - let app: String + let app: DIDWeb init( + account: Account, keyserver: URL, dappPubKey: DIDKey, - reason: String, - app: String + app: DIDWeb ) { + self.account = account self.keyserver = keyserver self.dappPubKey = dappPubKey - self.reason = reason self.app = app } init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) self.keyserver = try claims.ksu.asURL() self.dappPubKey = try DIDKey(did: claims.aud) - self.reason = claims.sub - self.app = claims.app + self.app = try DIDWeb(did: claims.app) } func encode(iss: String) throws -> Claims { @@ -70,8 +70,8 @@ struct NotifyDeletePayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: dappPubKey.did(variant: .ED25519), - sub: reason, - app: app + sub: account.did, + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift similarity index 82% rename from Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift index 83be1586c..b9f97cc43 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift @@ -14,7 +14,7 @@ struct NotifyDeleteResponsePayload: JWTClaimsCodable { let iss: String /// `did:key` of an identity key. Enables to resolve attached blockchain account. let aud: String - /// Hash of the existing subscription payload + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp's domain url let app: String @@ -36,14 +36,14 @@ struct NotifyDeleteResponsePayload: JWTClaimsCodable { } } + let account: Account let selfPubKey: DIDKey - let subscriptionHash: String - let app: String + let app: DIDWeb init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) - self.subscriptionHash = claims.sub - self.app = claims.app + self.app = try DIDWeb(did: claims.app) } func encode(iss: String) throws -> Claims { @@ -53,8 +53,8 @@ struct NotifyDeleteResponsePayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: selfPubKey.did(variant: .ED25519), - sub: subscriptionHash, - app: app + sub: account.did, + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessagePayload.swift similarity index 69% rename from Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessagePayload.swift index cf7b5687e..3e3679827 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessagePayload.swift @@ -12,9 +12,7 @@ struct NotifyMessagePayload: JWTClaimsCodable { /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. diddoc authentication key let iss: String - /// Blockchain account `did:pkh` - let aud: String - /// Subscription ID (sha256 hash of subscriptionAuth) + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp domain url let app: String @@ -38,17 +36,15 @@ struct NotifyMessagePayload: JWTClaimsCodable { } } - let castServerPubKey: DIDKey + let dappAuthenticationKey: DIDKey let account: Account - let subscriptionId: String - let app: String + let app: DIDWeb let message: NotifyMessage init(claims: Claims) throws { - self.castServerPubKey = try DIDKey(did: claims.iss) - self.account = try DIDPKH(did: claims.aud).account - self.subscriptionId = claims.sub - self.app = claims.app + self.dappAuthenticationKey = try DIDKey(did: claims.iss) + self.account = try DIDPKH(did: claims.sub).account + self.app = try DIDWeb(did: claims.app) self.message = claims.msg } @@ -57,10 +53,9 @@ struct NotifyMessagePayload: JWTClaimsCodable { iat: defaultIat(), exp: expiry(days: 1), act: Claims.action, - iss: castServerPubKey.multibase(variant: .ED25519), - aud: account.did, - sub: subscriptionId, - app: app, + iss: dappAuthenticationKey.multibase(variant: .ED25519), + sub: account.did, + app: app.did, msg: message ) } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessageReceiptPayload.swift similarity index 71% rename from Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessageReceiptPayload.swift index 57934d03c..985473cf5 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyMessageReceiptPayload.swift @@ -9,57 +9,57 @@ struct NotifyMessageReceiptPayload: JWTClaimsCodable { let exp: UInt64 /// Key server URL let ksu: String - /// Action intent (must be `notify_receipt`) + /// Action intent (must be `notify_message_response`) let act: String? /// `did:key` of an identity key. Enables to resolve attached blockchain account. let iss: String /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. let aud: String - /// Hash of the stringified notify message object received + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp's domain url let app: String static var action: String? { - return "notify_receipt" + return "notify_message_response" } } struct Wrapper: JWTWrapper { - let receiptAuth: String + let responseAuth: String init(jwtString: String) { - self.receiptAuth = jwtString + self.responseAuth = jwtString } var jwtString: String { - return receiptAuth + return responseAuth } } + let account: Account let keyserver: URL let dappPubKey: DIDKey - let messageHash: String - let app: String + let app: DIDWeb init( + account: Account, keyserver: URL, dappPubKey: DIDKey, - messageHash: String, - app: String + app: DIDWeb ) { + self.account = account self.keyserver = keyserver self.dappPubKey = dappPubKey - self.messageHash = messageHash self.app = app } init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) self.keyserver = try claims.ksu.asURL() self.dappPubKey = try DIDKey(did: claims.aud) - self.messageHash = claims.sub - self.app = claims.app + self.app = try DIDWeb(did: claims.app) } func encode(iss: String) throws -> Claims { @@ -70,8 +70,8 @@ struct NotifyMessageReceiptPayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: dappPubKey.did(variant: .ED25519), - sub: messageHash, - app: app + sub: account.did, + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionPayload.swift similarity index 93% rename from Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionPayload.swift index 847635da0..03e1d065a 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionPayload.swift @@ -43,14 +43,14 @@ struct NotifySubscriptionPayload: JWTClaimsCodable { let dappPubKey: DIDKey let keyserver: URL let subscriptionAccount: Account - let dappUrl: String + let app: DIDWeb let scope: String - init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { + init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, app: DIDWeb, scope: String) { self.dappPubKey = dappPubKey self.keyserver = keyserver self.subscriptionAccount = subscriptionAccount - self.dappUrl = dappUrl + self.app = app self.scope = scope } @@ -58,7 +58,7 @@ struct NotifySubscriptionPayload: JWTClaimsCodable { self.dappPubKey = try DIDKey(did: claims.aud) self.keyserver = try claims.ksu.asURL() self.subscriptionAccount = try Account(DIDPKHString: claims.sub) - self.dappUrl = claims.app + self.app = try DIDWeb(did: claims.app) self.scope = claims.scp } @@ -72,7 +72,7 @@ struct NotifySubscriptionPayload: JWTClaimsCodable { aud: dappPubKey.did(variant: .ED25519), sub: subscriptionAccount.did, scp: scope, - app: dappUrl + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift similarity index 87% rename from Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift index f972f345b..8ed7e775e 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift @@ -14,7 +14,7 @@ struct NotifySubscriptionResponsePayload: JWTClaimsCodable { let iss: String /// `did:key` of an identity key. Allows for the resolution of the attached blockchain account. let aud: String - /// `did:key` of the public key used for key agreement on the Notify topic + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp's domain url let app: String @@ -36,13 +36,13 @@ struct NotifySubscriptionResponsePayload: JWTClaimsCodable { } } + let account: Account let selfPubKey: DIDKey - let publicKey: DIDKey let app: String init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) - self.publicKey = try DIDKey(did: claims.sub) self.app = claims.app } @@ -53,7 +53,7 @@ struct NotifySubscriptionResponsePayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: selfPubKey.did(variant: .ED25519), - sub: publicKey.did(variant: .X25519), + sub: account.did, app: app ) } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdatePayload.swift similarity index 93% rename from Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdatePayload.swift index ef88ebc87..c3195c7d6 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdatePayload.swift @@ -43,14 +43,14 @@ struct NotifyUpdatePayload: JWTClaimsCodable { let dappPubKey: DIDKey let keyserver: URL let subscriptionAccount: Account - let dappUrl: String + let app: DIDWeb let scope: String - init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { + init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, app: DIDWeb, scope: String) { self.dappPubKey = dappPubKey self.keyserver = keyserver self.subscriptionAccount = subscriptionAccount - self.dappUrl = dappUrl + self.app = app self.scope = scope } @@ -58,7 +58,7 @@ struct NotifyUpdatePayload: JWTClaimsCodable { self.dappPubKey = try DIDKey(did: claims.aud) self.keyserver = try claims.ksu.asURL() self.subscriptionAccount = try Account(DIDPKHString: claims.sub) - self.dappUrl = claims.app + self.app = try DIDWeb(did: claims.app) self.scope = claims.scp } @@ -72,7 +72,7 @@ struct NotifyUpdatePayload: JWTClaimsCodable { aud: dappPubKey.did(variant: .ED25519), sub: subscriptionAccount.did, scp: scope, - app: dappUrl + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift similarity index 82% rename from Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift index 2153c7f9b..a03ca61f8 100644 --- a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift @@ -14,7 +14,7 @@ struct NotifyUpdateResponsePayload: JWTClaimsCodable { let iss: String /// `did:key` of an identity key. Enables to resolve attached blockchain account. let aud: String - /// Hash of the new subscription payload + /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String /// Dapp's domain url let app: String @@ -36,14 +36,14 @@ struct NotifyUpdateResponsePayload: JWTClaimsCodable { } } + let account: Account let selfPubKey: DIDKey - let subscriptionHash: String - let app: String + let app: DIDWeb init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) - self.subscriptionHash = claims.sub - self.app = claims.app + self.app = try DIDWeb(did: claims.app) } func encode(iss: String) throws -> Claims { @@ -53,8 +53,8 @@ struct NotifyUpdateResponsePayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: selfPubKey.did(variant: .ED25519), - sub: subscriptionHash, - app: app + sub: account.did, + app: app.did ) } } diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedRequestPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedRequestPayload.swift new file mode 100644 index 000000000..5dc241ecc --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedRequestPayload.swift @@ -0,0 +1,51 @@ +import Foundation + +struct NotifySubscriptionsChangedRequestPayload: JWTClaimsCodable { + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Action intent (must be `notify_subscriptions_changed_request`) + let act: String? + + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. diddoc authentication key + let iss: String + /// Blockchain account `did:pkh` + let aud: String + /// array of Notify Server Subscriptions + let sbs: [NotifyServerSubscription] + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + + static var action: String? { + return "notify_subscriptions_changed" + } + } + + struct Wrapper: JWTWrapper { + let subscriptionsChangedAuth: String + + init(jwtString: String) { + self.subscriptionsChangedAuth = jwtString + } + + var jwtString: String { + return subscriptionsChangedAuth + } + } + + let notifyServerAuthenticationKey: DIDKey + let subscriptions: [NotifyServerSubscription] + let account: Account + + init(claims: Claims) throws { + self.notifyServerAuthenticationKey = try DIDKey(did: claims.iss) + self.subscriptions = claims.sbs + self.account = try Account(DIDPKHString: claims.sub) + } + + func encode(iss: String) throws -> Claims { + fatalError("Client is not supposed to encode this JWT payload") + } +} diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedResponsePayload.swift new file mode 100644 index 000000000..0fd37fb8a --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_subscriptions_changed/NotifySubscriptionsChangedResponsePayload.swift @@ -0,0 +1,65 @@ +import Foundation + +struct NotifySubscriptionsChangedResponsePayload: JWTClaimsCodable { + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_subscriptions_changed_response` + let act: String? + + /// `did:key` of client identity key + let iss: String + /// `did:key` of Notify Server authentication key + let aud: String + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + + static var action: String? { + return "notify_subscriptions_changed_response" + } + } + + struct Wrapper: JWTWrapper { + let responseAuth: String + + init(jwtString: String) { + self.responseAuth = jwtString + } + + var jwtString: String { + return responseAuth + } + } + + init(account: Account, keyserver: URL, notifyServerAuthenticationKey: DIDKey) { + self.account = account + self.keyserver = keyserver + self.notifyServerAuthenticationKey = notifyServerAuthenticationKey + } + + let account: Account + let notifyServerAuthenticationKey: DIDKey + let keyserver: URL + + init(claims: Claims) throws { + fatalError("Method not expected to be called by the client") + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 30), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: notifyServerAuthenticationKey.did(variant: .ED25519), + sub: account.did + ) + } + +} + diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift new file mode 100644 index 000000000..0fad6272b --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift @@ -0,0 +1,87 @@ +import Foundation + +struct NotifyWatchSubscriptionsPayload: JWTClaimsCodable { + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_watch_subscriptions` + let act: String? + + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let iss: String + /// `did:key` of an identity key. + let aud: String + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + /// Dapp domain url + let app: String? + + static var action: String? { + return "notify_watch_subscriptions" + } + + // Note: - Overriding `encode(to encoder: Encoder)` implementation to force null app encoding + + enum CodingKeys: CodingKey { + case iat, exp, ksu, act, iss, aud, sub, app + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.self) + try container.encode(self.iat, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.iat) + try container.encode(self.exp, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.exp) + try container.encode(self.ksu, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.ksu) + try container.encodeIfPresent(self.act, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.act) + try container.encode(self.iss, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.iss) + try container.encode(self.aud, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.aud) + try container.encode(self.sub, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.sub) + try container.encode(self.app, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.app) + } + } + + struct Wrapper: JWTWrapper { + let watchSubscriptionsAuth: String + + init(jwtString: String) { + self.watchSubscriptionsAuth = jwtString + } + + var jwtString: String { + return watchSubscriptionsAuth + } + } + + let notifyServerIdentityKey: DIDKey + let keyserver: URL + let subscriptionAccount: Account + + init(notifyServerAuthenticationKey: DIDKey, keyserver: URL, subscriptionAccount: Account) { + self.notifyServerIdentityKey = notifyServerAuthenticationKey + self.keyserver = keyserver + self.subscriptionAccount = subscriptionAccount + } + + init(claims: Claims) throws { + self.notifyServerIdentityKey = try DIDKey(did: claims.aud) + self.keyserver = try claims.ksu.asURL() + self.subscriptionAccount = try Account(DIDPKHString: claims.sub) + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 30), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: notifyServerIdentityKey.did(variant: .ED25519), + sub: subscriptionAccount.did, + app: nil + ) + } + +} diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsResponsePayload.swift new file mode 100644 index 000000000..57e3f48f5 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsResponsePayload.swift @@ -0,0 +1,60 @@ +import Foundation + +struct NotifyWatchSubscriptionsResponsePayload: JWTClaimsCodable { + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Description of action intent. Must be equal to `notify_watch_subscriptions_response` + let act: String? + + /// `did:key` of Notify Server authentication key + let iss: String + /// `did:key` of an identity key. + let aud: String + /// array of Notify Subscriptions + let sbs: [NotifyServerSubscription] + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + + static var action: String? { + return "notify_watch_subscriptions_response" + } + } + + struct Wrapper: JWTWrapper { + let responseAuth: String + + init(jwtString: String) { + self.responseAuth = jwtString + } + + var jwtString: String { + return responseAuth + } + } + + let account: Account + let subscriptions: [NotifyServerSubscription] + let selfIdentityKey: DIDKey + + init(claims: Claims) throws { + self.account = try Account(DIDPKHString: claims.sub) + self.selfIdentityKey = try DIDKey(did: claims.aud) + self.subscriptions = claims.sbs + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 30), + act: Claims.action, + iss: iss, + aud: selfIdentityKey.did(variant: .ED25519), + sbs: subscriptions, + sub: account.did + ) + } + +} diff --git a/Sources/WalletConnectNotify/Types/NotificationConfig.swift b/Sources/WalletConnectNotify/Types/NotificationConfig.swift deleted file mode 100644 index f17133c93..000000000 --- a/Sources/WalletConnectNotify/Types/NotificationConfig.swift +++ /dev/null @@ -1,8 +0,0 @@ - -import Foundation - -struct NotificationConfig: Codable { - let version: Int - let lastModified: TimeInterval - let types: [NotificationType] -} diff --git a/Sources/WalletConnectNotify/Types/NotificationType.swift b/Sources/WalletConnectNotify/Types/NotificationType.swift deleted file mode 100644 index b741c4a2f..000000000 --- a/Sources/WalletConnectNotify/Types/NotificationType.swift +++ /dev/null @@ -1,7 +0,0 @@ - -import Foundation - -public struct NotificationType: Codable, Hashable { - let name: String - let description: String -} diff --git a/Sources/WalletConnectPush/PushClient.swift b/Sources/WalletConnectPush/PushClient.swift index 2eb23aacd..30f87f9f3 100644 --- a/Sources/WalletConnectPush/PushClient.swift +++ b/Sources/WalletConnectPush/PushClient.swift @@ -1,10 +1,17 @@ import Foundation +import Combine public class PushClient: PushClientProtocol { private let registerService: PushRegisterService + private let logger: ConsoleLogging - init(registerService: PushRegisterService) { + public var logsPublisher: AnyPublisher { + return logger.logsPublisher + } + + init(registerService: PushRegisterService, logger: ConsoleLogging) { self.registerService = registerService + self.logger = logger } public func register(deviceToken: Data) async throws { diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift index 50a0145f9..fb2fb0d10 100644 --- a/Sources/WalletConnectPush/PushClientFactory.swift +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -6,10 +6,12 @@ public struct PushClientFactory { environment: APNSEnvironment) -> PushClient { let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + let keyValueStorage = UserDefaults.standard return PushClientFactory.create( projectId: projectId, pushHost: pushHost, + keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, environment: environment) } @@ -17,6 +19,7 @@ public struct PushClientFactory { public static func create( projectId: String, pushHost: String, + keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, environment: APNSEnvironment ) -> PushClient { @@ -28,12 +31,12 @@ public struct PushClientFactory { let logger = ConsoleLogger(prefix: "👂🏻", loggingLevel: .off) let httpClient = HTTPNetworkClient(host: pushHost, session: session) - let clientIdStorage = ClientIdStorage(keychain: keychainStorage) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychainStorage, logger: logger) let pushAuthenticator = PushAuthenticator(clientIdStorage: clientIdStorage, pushHost: pushHost) let registerService = PushRegisterService(httpClient: httpClient, projectId: projectId, clientIdStorage: clientIdStorage, pushAuthenticator: pushAuthenticator, logger: logger, environment: environment) - return PushClient(registerService: registerService) + return PushClient(registerService: registerService, logger: logger) } } diff --git a/Sources/WalletConnectPush/Register/PushRegisterService.swift b/Sources/WalletConnectPush/Register/PushRegisterService.swift index 2e142ae26..3c06ce281 100644 --- a/Sources/WalletConnectPush/Register/PushRegisterService.swift +++ b/Sources/WalletConnectPush/Register/PushRegisterService.swift @@ -45,13 +45,15 @@ actor PushRegisterService { guard response.status == .success else { throw Errors.registrationFailed } - logger.debug("Successfully registered at Echo Server") + logger.debug("Successfully registered at Push Server") } catch { if (error as? HTTPError) == .couldNotConnect && !fallback { + logger.debug("Trying fallback") fallback = true await echoHostFallback() try await register(deviceToken: deviceToken) } + logger.debug("Push Server registration error: \(error.localizedDescription)") throw error } } diff --git a/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift index 2055e1813..87a7daada 100644 --- a/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift @@ -1,21 +1,23 @@ import Foundation public protocol ClientIdAuthenticating { - func createAuthToken() throws -> String + func createAuthToken(url: String?) throws -> String } -public struct ClientIdAuthenticator: ClientIdAuthenticating { +public final class ClientIdAuthenticator: ClientIdAuthenticating { private let clientIdStorage: ClientIdStoring - private let url: String + private var url: String public init(clientIdStorage: ClientIdStoring, url: String) { self.clientIdStorage = clientIdStorage self.url = url } - public func createAuthToken() throws -> String { + public func createAuthToken(url: String? = nil) throws -> String { + url.flatMap { self.url = $0 } + let keyPair = try clientIdStorage.getOrCreateKeyPair() - let payload = RelayAuthPayload(subject: getSubject(), audience: url) + let payload = RelayAuthPayload(subject: getSubject(), audience: self.url) return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString } diff --git a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift index 2e3c9cd7f..272451b96 100644 --- a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift @@ -6,26 +6,89 @@ public protocol ClientIdStoring { } public struct ClientIdStorage: ClientIdStoring { - private let key = "com.walletconnect.iridium.client_id" + private let oldStorageKey = "com.walletconnect.iridium.client_id" + private let publicStorageKey = "com.walletconnect.iridium.client_id.public" + + private let defaults: KeyValueStorage private let keychain: KeychainStorageProtocol + private let logger: ConsoleLogging - public init(keychain: KeychainStorageProtocol) { + public init(defaults: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) { + self.defaults = defaults self.keychain = keychain + self.logger = logger + + migrateIfNeeded() } public func getOrCreateKeyPair() throws -> SigningPrivateKey { do { - return try keychain.read(key: key) + let publicPart = try getPublicPart() + return try getPrivatePart(for: publicPart) } catch { let privateKey = SigningPrivateKey() - try keychain.add(privateKey, forKey: key) + try setPrivatePart(privateKey) + setPublicPart(privateKey.publicKey) return privateKey } } public func getClientId() throws -> String { - let privateKey: SigningPrivateKey = try keychain.read(key: key) - let pubKey = privateKey.publicKey.rawRepresentation - return DIDKey(rawData: pubKey).did(variant: .ED25519) + let pubKey = try getPublicPart() + let _ = try getPrivatePart(for: pubKey) + return DIDKey(rawData: pubKey.rawRepresentation).did(variant: .ED25519) + } +} + +private extension ClientIdStorage { + + enum Errors: Error { + case publicPartNotFound + case privatePartNotFound + } + + func migrateIfNeeded() { + guard let privateKey: SigningPrivateKey = try? keychain.read(key: oldStorageKey) else { + return + } + + do { + try setPrivatePart(privateKey) + setPublicPart(privateKey.publicKey) + try keychain.delete(key: oldStorageKey) + logger.debug("ClientID migrated") + } catch { + logger.debug("ClientID migration failed with: \(error.localizedDescription)") + } + } + + func getPublicPart() throws -> SigningPublicKey { + guard let data = defaults.data(forKey: publicStorageKey) else { + throw Errors.publicPartNotFound + } + return try SigningPublicKey(rawRepresentation: data) + } + + func setPublicPart(_ newValue: SigningPublicKey) { + defaults.set(newValue.rawRepresentation, forKey: publicStorageKey) + } + + func getPrivatePart(for publicPart: SigningPublicKey) throws -> SigningPrivateKey { + do { + return try keychain.read(key: publicPart.storageId) + } catch { + throw Errors.privatePartNotFound + } + } + + func setPrivatePart(_ newValue: SigningPrivateKey) throws { + try keychain.add(newValue, forKey: newValue.publicKey.storageId) + } +} + +private extension SigningPublicKey { + + var storageId: String { + return rawRepresentation.sha256().toHexString() } } diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index e32e2e114..52e73e014 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.7.2"} +{"version": "1.9.0"} diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index bdc87644e..64be1d87d 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -37,7 +37,7 @@ public struct RelayClientFactory { logger: ConsoleLogging ) -> RelayClient { - let clientIdStorage = ClientIdStorage(keychain: keychainStorage) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychainStorage, logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, diff --git a/Sources/WalletConnectRelay/RelayURLFactory.swift b/Sources/WalletConnectRelay/RelayURLFactory.swift index e1f852e3d..44e34500e 100644 --- a/Sources/WalletConnectRelay/RelayURLFactory.swift +++ b/Sources/WalletConnectRelay/RelayURLFactory.swift @@ -23,7 +23,7 @@ struct RelayUrlFactory { URLQueryItem(name: "projectId", value: projectId) ] do { - let authToken = try socketAuthenticator.createAuthToken() + let authToken = try socketAuthenticator.createAuthToken(url: fallback ? "wss://" + NetworkConstants.fallbackUrl : "wss://" + relayHost) components.queryItems?.append(URLQueryItem(name: "auth", value: authToken)) } catch { // TODO: Handle token creation errors diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index dd77da949..7e26a2725 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -314,15 +314,16 @@ private extension ApproveEngine { Task(priority: .high) { let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let origin = try await verifyClient.verifyOrigin(assertionId: assertionId) + let response = try await verifyClient.verifyOrigin(assertionId: assertionId) let verifyContext = verifyClient.createVerifyContext( - origin: origin, - domain: payload.request.proposer.metadata.url + origin: response.origin, + domain: payload.request.proposer.metadata.url, + isScam: response.isScam ) verifyContextStore.set(verifyContext, forKey: proposal.proposer.publicKey) onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.proposer.metadata.url) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.proposer.metadata.url, isScam: nil) onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) return } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index b90a06fb5..3e6cec6bf 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -246,12 +246,12 @@ private extension SessionEngine { Task(priority: .high) { let assertionId = payload.decryptedPayload.sha256().toHexString() do { - let origin = try await verifyClient.verifyOrigin(assertionId: assertionId) - let verifyContext = verifyClient.createVerifyContext(origin: origin, domain: session.peerParticipant.metadata.url) + let response = try await verifyClient.verifyOrigin(assertionId: assertionId) + let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: session.peerParticipant.metadata.url, isScam: response.isScam) verifyContextStore.set(verifyContext, forKey: request.id.string) onSessionRequest?(request, verifyContext) } catch { - let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: session.peerParticipant.metadata.url) + let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: session.peerParticipant.metadata.url, isScam: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) onSessionRequest?(request, verifyContext) return diff --git a/Sources/WalletConnectSign/Services/App/AppProposeService.swift b/Sources/WalletConnectSign/Services/App/AppProposeService.swift index 541999a1a..a92b80bea 100644 --- a/Sources/WalletConnectSign/Services/App/AppProposeService.swift +++ b/Sources/WalletConnectSign/Services/App/AppProposeService.swift @@ -43,7 +43,7 @@ final class AppProposeService { relays: [relay], proposer: proposer, requiredNamespaces: namespaces, - optionalNamespaces: optionalNamespaces, + optionalNamespaces: optionalNamespaces ?? [:], sessionProperties: sessionProperties ) diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift index 5a47a81d1..2a5974471 100644 --- a/Sources/WalletConnectSign/Services/HistoryService.swift +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -92,7 +92,7 @@ private extension HistoryService { pairingTopic: record.topic, proposer: proposal.proposer.metadata, requiredNamespaces: proposal.requiredNamespaces, - optionalNamespaces: proposal.optionalNamespaces, + optionalNamespaces: proposal.optionalNamespaces ?? [:], sessionProperties: proposal.sessionProperties, proposal: proposal ) diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift index 437c115c6..95f56ee76 100644 --- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -10,6 +10,7 @@ public protocol SignClientProtocol { var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { get } var sessionResponsePublisher: AnyPublisher { get } var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } + var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> { get } func connect(requiredNamespaces: [String: ProposalNamespace], optionalNamespaces: [String: ProposalNamespace]?, sessionProperties: [String: String]?, topic: String) async throws func request(params: Request) async throws diff --git a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift index 9a10e3380..fa1ee979a 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift @@ -13,7 +13,7 @@ struct SessionProposal: Codable, Equatable { pairingTopic: pairingTopic, proposer: proposer.metadata, requiredNamespaces: requiredNamespaces, - optionalNamespaces: optionalNamespaces, + optionalNamespaces: optionalNamespaces ?? [:], sessionProperties: sessionProperties, proposal: self ) diff --git a/Sources/WalletConnectUtils/DID/DIDPKH.swift b/Sources/WalletConnectUtils/DID/DIDPKH.swift index ffb428234..3930d257b 100644 --- a/Sources/WalletConnectUtils/DID/DIDPKH.swift +++ b/Sources/WalletConnectUtils/DID/DIDPKH.swift @@ -10,7 +10,7 @@ public struct DIDPKH { } public let account: Account - public let string: String + public let did: String public init(did: String) throws { guard did.starts(with: DIDPKH.didPrefix) @@ -22,12 +22,12 @@ public struct DIDPKH { guard let account = Account(string) else { throw Errors.invalidAccount } - self.string = string + self.did = did self.account = account } public init(account: Account) { - self.string = "\(DIDPKH.didPrefix):\(account.absoluteString)" + self.did = "\(DIDPKH.didPrefix):\(account.absoluteString)" self.account = account } } @@ -39,6 +39,6 @@ extension Account { } public var did: String { - return DIDPKH(account: self).string + return DIDPKH(account: self).did } } diff --git a/Sources/WalletConnectUtils/DID/DIDWeb.swift b/Sources/WalletConnectUtils/DID/DIDWeb.swift new file mode 100644 index 000000000..61d46ab9b --- /dev/null +++ b/Sources/WalletConnectUtils/DID/DIDWeb.swift @@ -0,0 +1,32 @@ +import Foundation + +public struct DIDWeb { + + public let host: String + + public init(url: URL) throws { + guard let host = url.host else { throw Errors.invalidUrl } + self.host = host + } + + public init(did: String) throws { + guard let host = did.components(separatedBy: ":").last else { throw Errors.invalidDid } + self.host = host + } + + public init(host: String) { + self.host = host + } + + public var did: String { + return "did:web:\(host)" + } +} + +extension DIDWeb { + + enum Errors: Error { + case invalidUrl + case invalidDid + } +} diff --git a/Sources/WalletConnectUtils/Extensions/Task.swift b/Sources/WalletConnectUtils/Extensions/Task.swift new file mode 100644 index 000000000..180157949 --- /dev/null +++ b/Sources/WalletConnectUtils/Extensions/Task.swift @@ -0,0 +1,24 @@ +import Foundation + +public typealias CancellableTask = Task + +extension Task where Success == Void, Failure == Never { + + public final class DisposeBag { + private var set: Set = [] + + public init() { } + + func insert(task: Task) { + set.insert(task) + } + + deinit { + set.forEach { $0.cancel() } + } + } + + public func store(in set: inout DisposeBag) { + set.insert(task: self) + } +} diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index c619d62d5..5677946ae 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -61,9 +61,20 @@ public class KeyedDatabase where Element: DatabaseObject { var map = index[key] ?? [:] for element in elements { - guard - map[element.databaseId] == nil else { continue } - map[element.databaseId] = element + map[element.databaseId] = element + } + + index[key] = map + + return true + } + + @discardableResult + public func replace(elements: [Element], for key: String) -> Bool { + var map: [String: Element] = [:] + + for element in elements { + map[element.databaseId] = element } index[key] = map diff --git a/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift b/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift index bfd5ccd26..a9f6b348d 100644 --- a/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift +++ b/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift @@ -111,8 +111,8 @@ extension ConsoleLogger: ConsoleLogging { #if DEBUG public struct ConsoleLoggerMock: ConsoleLogging { - public var logsPublisher: AnyPublisher { - return PassthroughSubject().eraseToAnyPublisher() + public var logsPublisher: AnyPublisher { + return PassthroughSubject().eraseToAnyPublisher() } public init() {} diff --git a/Sources/WalletConnectUtils/Logger/Log.swift b/Sources/WalletConnectUtils/Logger/Log.swift index 5e3c7f785..8a3bda077 100644 --- a/Sources/WalletConnectUtils/Logger/Log.swift +++ b/Sources/WalletConnectUtils/Logger/Log.swift @@ -1,21 +1,28 @@ import Foundation public struct LogMessage { + public let message: String public let properties: [String: String]? - public var aggregated: String { - var aggregatedProperties = "" - properties?.forEach { key, value in - aggregatedProperties += "\(key): \(value), " - } + public var aggregated: String { + var aggregatedProperties = "" - if !aggregatedProperties.isEmpty { - aggregatedProperties = String(aggregatedProperties.dropLast(2)) - } + properties?.forEach { key, value in + aggregatedProperties += "\(key): \(value), " + } - return "\(message), properties: [\(aggregatedProperties)]" + if !aggregatedProperties.isEmpty { + aggregatedProperties = String(aggregatedProperties.dropLast(2)) } + + return "\(message), properties: [\(aggregatedProperties)]" + } + + public init(message: String, properties: [String : String]? = nil) { + self.message = message + self.properties = properties + } } public enum Log { diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift index 604929ff0..2e16a652d 100644 --- a/Sources/WalletConnectUtils/WalletConnectURI.swift +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -1,7 +1,6 @@ import Foundation public struct WalletConnectURI: Equatable { - public let topic: String public let version: String public let symKey: String @@ -44,6 +43,13 @@ public struct WalletConnectURI: Equatable { self.symKey = symKey self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) } + + public init?(deeplinkUri: URL) { + if let deeplinkUri = deeplinkUri.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: deeplinkUri) + } + return nil + } private var relayQuery: String { var query = "relay-protocol=\(relay.protocol)" @@ -61,3 +67,25 @@ public struct WalletConnectURI: Equatable { return URLComponents(string: urlString) } } + +#if canImport(UIKit) + +import UIKit + +extension WalletConnectURI { + public init?(connectionOptions: UIScene.ConnectionOptions) { + if let uri = connectionOptions.urlContexts.first?.url.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: uri) + } + return nil + } + + public init?(urlContext: UIOpenURLContext) { + if let uri = urlContext.url.query?.replacingOccurrences(of: "uri=", with: "") { + self.init(string: uri) + } + return nil + } +} + +#endif diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift index 2ab3267ae..0689088d2 100644 --- a/Sources/WalletConnectVerify/OriginVerifier.swift +++ b/Sources/WalletConnectVerify/OriginVerifier.swift @@ -5,16 +5,12 @@ public final class OriginVerifier { case registrationFailed } - private var verifyHost: String + private var verifyHost = "verify.walletconnect.com" /// 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 { + + func verifyOrigin(assertionId: String) async throws -> VerifyResponse { let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.timeoutIntervalForRequest = 5.0 sessionConfiguration.timeoutIntervalForResource = 5.0 @@ -27,10 +23,10 @@ public final class OriginVerifier { VerifyResponse.self, at: VerifyAPI.resolve(assertionId: assertionId) ) - guard let origin = response.origin else { + guard let _ = response.origin else { throw Errors.registrationFailed } - return origin + return response } catch { if (error as? HTTPError) == .couldNotConnect && !fallback { fallback = true diff --git a/Sources/WalletConnectVerify/Register/VerifyResponse.swift b/Sources/WalletConnectVerify/Register/VerifyResponse.swift index ed2e59460..24fba83eb 100644 --- a/Sources/WalletConnectVerify/Register/VerifyResponse.swift +++ b/Sources/WalletConnectVerify/Register/VerifyResponse.swift @@ -1,5 +1,6 @@ import Foundation -struct VerifyResponse: Decodable { - let origin: String? +public struct VerifyResponse: Decodable { + public let origin: String? + public let isScam: Bool? } diff --git a/Sources/WalletConnectVerify/VerifyClient.swift b/Sources/WalletConnectVerify/VerifyClient.swift index b1364f347..e528f2b60 100644 --- a/Sources/WalletConnectVerify/VerifyClient.swift +++ b/Sources/WalletConnectVerify/VerifyClient.swift @@ -2,8 +2,8 @@ import DeviceCheck import Foundation public protocol VerifyClientProtocol { - func verifyOrigin(assertionId: String) async throws -> String - func createVerifyContext(origin: String?, domain: String) -> VerifyContext + func verifyOrigin(assertionId: String) async throws -> VerifyResponse + func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext } public actor VerifyClient: VerifyClientProtocol { @@ -14,16 +14,12 @@ public actor VerifyClient: VerifyClientProtocol { let originVerifier: OriginVerifier let assertionRegistrer: AssertionRegistrer let appAttestationRegistrer: AppAttestationRegistrer - - private let verifyHost: String init( - verifyHost: String, originVerifier: OriginVerifier, assertionRegistrer: AssertionRegistrer, appAttestationRegistrer: AppAttestationRegistrer ) { - self.verifyHost = verifyHost self.originVerifier = originVerifier self.assertionRegistrer = assertionRegistrer self.appAttestationRegistrer = appAttestationRegistrer @@ -33,22 +29,26 @@ public actor VerifyClient: VerifyClientProtocol { try await appAttestationRegistrer.registerAttestationIfNeeded() } - public func verifyOrigin(assertionId: String) async throws -> String { + public func verifyOrigin(assertionId: String) async throws -> VerifyResponse { return try await originVerifier.verifyOrigin(assertionId: assertionId) } - nonisolated public func createVerifyContext(origin: String?, domain: String) -> VerifyContext { + nonisolated public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { + guard isScam == nil else { + return VerifyContext( + origin: origin, + validation: .scam + ) + } if let origin, let originUrl = URL(string: origin), let domainUrl = URL(string: domain) { return VerifyContext( origin: origin, - validation: (originUrl.host == domainUrl.host) ? .valid : .invalid, - verifyUrl: verifyHost + validation: (originUrl.host == domainUrl.host) ? .valid : .invalid ) } else { return VerifyContext( origin: origin, - validation: .unknown, - verifyUrl: verifyHost + validation: .unknown ) } } @@ -63,12 +63,12 @@ public actor VerifyClient: VerifyClientProtocol { public struct VerifyClientMock: VerifyClientProtocol { public init() {} - public func verifyOrigin(assertionId: String) async throws -> String { - return "domain.com" + public func verifyOrigin(assertionId: String) async throws -> VerifyResponse { + return VerifyResponse(origin: "domain.com", isScam: nil) } - public func createVerifyContext(origin: String?, domain: String) -> VerifyContext { - return VerifyContext(origin: "domain.com", validation: .valid, verifyUrl: "verify.walletconnect.com") + public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { + return VerifyContext(origin: "domain.com", validation: .valid) } } diff --git a/Sources/WalletConnectVerify/VerifyClientFactory.swift b/Sources/WalletConnectVerify/VerifyClientFactory.swift index 1d00a6af1..8b230fc5f 100644 --- a/Sources/WalletConnectVerify/VerifyClientFactory.swift +++ b/Sources/WalletConnectVerify/VerifyClientFactory.swift @@ -1,8 +1,8 @@ import Foundation public class VerifyClientFactory { - public static func create(verifyHost: String = "verify.walletconnect.com") -> VerifyClient { - let originVerifier = OriginVerifier(verifyHost: verifyHost) + public static func create() -> VerifyClient { + let originVerifier = OriginVerifier() let assertionRegistrer = AssertionRegistrer() let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard @@ -18,7 +18,6 @@ public class VerifyClientFactory { keyAttestationService: keyAttestationService ) return VerifyClient( - verifyHost: verifyHost, originVerifier: originVerifier, assertionRegistrer: assertionRegistrer, appAttestationRegistrer: appAttestationRegistrer diff --git a/Sources/WalletConnectVerify/VerifyContext.swift b/Sources/WalletConnectVerify/VerifyContext.swift index 62b0be4a2..e85613493 100644 --- a/Sources/WalletConnectVerify/VerifyContext.swift +++ b/Sources/WalletConnectVerify/VerifyContext.swift @@ -3,15 +3,14 @@ public struct VerifyContext: Equatable, Hashable, Codable { case unknown case valid case invalid + case scam } public let origin: String? public let validation: ValidationStatus - public let verifyUrl: String - public init(origin: String?, validation: ValidationStatus, verifyUrl: String) { + public init(origin: String?, validation: ValidationStatus) { self.origin = origin self.validation = validation - self.verifyUrl = verifyUrl } } diff --git a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift index b6fd91d00..62d8a60c0 100644 --- a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift +++ b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift @@ -34,7 +34,7 @@ final class ChatClientProxy { case .register: let params = try parse(RegisterRequest.self, params: request.params) - try await client.register(account: params.account, onSign: onSign) + try await client.register(account: params.account, domain: params.domain, onSign: onSign) try await respond(request: request) case .resolve: @@ -81,6 +81,7 @@ private extension ChatClientProxy { struct RegisterRequest: Codable { let account: Account + let domain: String } struct ResolveRequest: Codable { diff --git a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift index 51481a2b7..427ba04e0 100644 --- a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift @@ -25,7 +25,7 @@ final class NotifyClientProxy { try await respond(request: request) case .subscribe: let params = try parse(SubscribeRequest.self, params: request.params) - try await client.subscribe(metadata: params.metadata, account: params.account) + try await client.subscribe(appDomain: appDomain, account: params.account) try await respond(request: request) case .getActiveSubscriptions: let subscriptions = client.getActiveSubscriptions() @@ -44,7 +44,7 @@ final class NotifyClientProxy { try await respond(request: request) case .register: let params = try parse(RegisterRequest.self, params: request.params) - try await client.register(account: params.account, onSign: onSign) + try await client.register(account: params.account, domain: params.domain, isLimited: params.isLimited, onSign: onSign) try await respond(request: request) } } @@ -73,7 +73,7 @@ private extension NotifyClientProxy { } struct SubscribeRequest: Codable { - let metadata: AppMetadata + let appDomain: String let account: Account } @@ -91,6 +91,8 @@ private extension NotifyClientProxy { struct RegisterRequest: Codable { let account: Account + let domain: String + let isLimited: Bool } func parse(_ type: Request.Type, params: AnyCodable?) throws -> Request { diff --git a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift index b152330ce..ddac26a48 100644 --- a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift @@ -5,6 +5,7 @@ enum NotifyClientRequest: String { case notifyUpdate = "notify_update" case notifyDelete = "notify_delete" case notifySubscription = "notify_subscription" + case notifySubscriptionsChanged = "notify_subscriptions_changed" var method: String { return rawValue diff --git a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift index 8b255bd81..a9c4c1558 100644 --- a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift @@ -33,6 +33,9 @@ final class NotifyClientRequestSubscriber { client.updateSubscriptionPublisher.sink { [unowned self] subscription in handle(event: .notifyUpdate, params: subscription) }.store(in: &publishers) + client.subscriptionsPublisher.sink { [unowned self] subscriptions in + handle(event: .notifySubscriptionsChanged, params: subscriptions) + }.store(in: &publishers) } } diff --git a/Sources/Web3Inbox/Web3InboxClientFactory.swift b/Sources/Web3Inbox/Web3InboxClientFactory.swift index 83ce998fe..9734edd3f 100644 --- a/Sources/Web3Inbox/Web3InboxClientFactory.swift +++ b/Sources/Web3Inbox/Web3InboxClientFactory.swift @@ -40,7 +40,7 @@ final class Web3InboxClientFactory { } private static func buildUrl(account: Account, config: [ConfigParam: Bool]) -> URL { - var urlComponents = URLComponents(string: "https://web3inbox-dev-hidden.vercel.app/")! + var urlComponents = URLComponents(string: "https://web3inbox-dev-hidden-git-chore-notif-refa-effa6b-walletconnect1.vercel.app/")! var queryItems = [URLQueryItem(name: "chatProvider", value: "ios"), URLQueryItem(name: "notifyProvider", value: "ios"), URLQueryItem(name: "account", value: account.address), URLQueryItem(name: "authProvider", value: "ios")] for param in config.filter({ $0.value == false}) { diff --git a/Tests/ChatTests/RegistryServiceTests.swift b/Tests/ChatTests/RegistryServiceTests.swift index 24d2c657e..8f6cb51b7 100644 --- a/Tests/ChatTests/RegistryServiceTests.swift +++ b/Tests/ChatTests/RegistryServiceTests.swift @@ -43,39 +43,39 @@ final class RegistryServiceTests: XCTestCase { resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, logger: ConsoleLoggerMock()) } - func testRegister() async throws { - let pubKey = try await identityClient.register(account: account, onSign: onSign) - - XCTAssertTrue(networkService.callRegisterIdentity) - - let identityKey = try identityStorage.getIdentityKey(for: account) - XCTAssertEqual(identityKey.publicKey.hexRepresentation, pubKey) - } - - func testGoPublic() async throws { - XCTAssertTrue(networkingInteractor.subscriptions.isEmpty) - - _ = try await identityClient.register(account: account, onSign: onSign) - let inviteKey = try await identityClient.goPublic(account: account) - try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) - - XCTAssertNoThrow(try identityStorage.getInviteKey(for: account)) - XCTAssertTrue(networkService.callRegisterInvite) - - XCTAssertEqual(networkingInteractor.subscriptions.count, 1) - XCTAssertNotNil(kms.getPublicKey(for: networkingInteractor.subscriptions[0])) - } - - func testUnregister() async throws { - XCTAssertThrowsError(try identityStorage.getIdentityKey(for: account)) - - _ = try await identityClient.register(account: account, onSign: onSign) - XCTAssertNoThrow(try identityStorage.getIdentityKey(for: account)) - - try await identityClient.unregister(account: account, onSign: onSign) - XCTAssertThrowsError(try identityStorage.getIdentityKey(for: account)) - XCTAssertTrue(networkService.callRemoveIdentity) - } +// func testRegister() async throws { +// let pubKey = try await identityClient.register(account: account, onSign: onSign) +// +// XCTAssertTrue(networkService.callRegisterIdentity) +// +// let identityKey = try identityStorage.getIdentityKey(for: account) +// XCTAssertEqual(identityKey.publicKey.hexRepresentation, pubKey) +// } + +// func testGoPublic() async throws { +// XCTAssertTrue(networkingInteractor.subscriptions.isEmpty) +// +// _ = try await identityClient.register(account: account, onSign: onSign) +// let inviteKey = try await identityClient.goPublic(account: account) +// try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) +// +// XCTAssertNoThrow(try identityStorage.getInviteKey(for: account)) +// XCTAssertTrue(networkService.callRegisterInvite) +// +// XCTAssertEqual(networkingInteractor.subscriptions.count, 1) +// XCTAssertNotNil(kms.getPublicKey(for: networkingInteractor.subscriptions[0])) +// } + +// func testUnregister() async throws { +// XCTAssertThrowsError(try identityStorage.getIdentityKey(for: account)) +// +// _ = try await identityClient.register(account: account, onSign: onSign) +// XCTAssertNoThrow(try identityStorage.getIdentityKey(for: account)) +// +// try await identityClient.unregister(account: account, onSign: onSign) +// XCTAssertThrowsError(try identityStorage.getIdentityKey(for: account)) +// XCTAssertTrue(networkService.callRemoveIdentity) +// } func testGoPrivate() async throws { let invitePubKey = try AgreementPublicKey(hex: inviteKeyStub) diff --git a/Tests/CommonsTests/AnyCodableTests.swift b/Tests/CommonsTests/AnyCodableTests.swift index bce2de631..9fe96b88d 100644 --- a/Tests/CommonsTests/AnyCodableTests.swift +++ b/Tests/CommonsTests/AnyCodableTests.swift @@ -28,7 +28,7 @@ private struct SampleStruct: Codable, Equatable { SampleStruct( bool: true, int: 1337, - double: 13.37, + double: 13, string: "verystringwow", object: SubObject( string: "0xdeadbeef" @@ -40,7 +40,7 @@ private struct SampleStruct: Codable, Equatable { { "bool": true, "int": 1337, - "double": 13.37, + "double": 13, "string": "verystringwow", "object": { "string": "0xdeadbeef" @@ -52,7 +52,7 @@ private struct SampleStruct: Codable, Equatable { { "bool": ****, "int": 1337, - "double": 13.37, + "double": 13, "string": "verystringwow", } """.data(using: .utf8)! diff --git a/Tests/NotifyTests/Mocks/MockNotifyStoring.swift b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift index 4f1be0991..bd773936c 100644 --- a/Tests/NotifyTests/Mocks/MockNotifyStoring.swift +++ b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift @@ -2,20 +2,25 @@ import Foundation @testable import WalletConnectNotify class MockNotifyStoring: NotifyStoring { + var subscriptions: [NotifySubscription] init(subscriptions: [NotifySubscription]) { self.subscriptions = subscriptions } - func getSubscriptions() -> [NotifySubscription] { - return subscriptions + func getSubscriptions(account: Account) -> [NotifySubscription] { + return subscriptions.filter { $0.account == account } } func getSubscription(topic: String) -> NotifySubscription? { return subscriptions.first { $0.topic == topic } } + func getAllSubscriptions() -> [WalletConnectNotify.NotifySubscription] { + return subscriptions + } + func setSubscription(_ subscription: NotifySubscription) async throws { if let index = subscriptions.firstIndex(where: { $0.topic == subscription.topic }) { subscriptions[index] = subscription @@ -24,6 +29,10 @@ class MockNotifyStoring: NotifyStoring { } } + func clearDatabase(account: WalletConnectUtils.Account) { + subscriptions = subscriptions.filter { $0.account != account } + } + func deleteSubscription(topic: String) async throws { subscriptions.removeAll(where: { $0.topic == topic }) } diff --git a/Tests/NotifyTests/Stubs/NotifySubscription.swift b/Tests/NotifyTests/Stubs/NotifySubscription.swift index 7251c8477..3e9a1892e 100644 --- a/Tests/NotifyTests/Stubs/NotifySubscription.swift +++ b/Tests/NotifyTests/Stubs/NotifySubscription.swift @@ -13,7 +13,7 @@ extension NotifySubscription { account: account, relay: relay, metadata: metadata, - scope: ["test": ScopeValue(description: "desc", enabled: true)], + scope: ["test": ScopeValue(id: "id", name: "name", description: "desc", enabled: true)], expiry: expiry, symKey: symKey ) diff --git a/Tests/NotifyTests/SubscriptionWatcherTests.swift b/Tests/NotifyTests/SubscriptionWatcherTests.swift new file mode 100644 index 000000000..e2d389759 --- /dev/null +++ b/Tests/NotifyTests/SubscriptionWatcherTests.swift @@ -0,0 +1,74 @@ +import Foundation +import XCTest +import TestingUtils +@testable import WalletConnectNotify + +class SubscriptionWatcherTests: XCTestCase { + + var sut: SubscriptionWatcher! + var mockRequester: MockNotifyWatchSubscriptionsRequester! + var mockLogger: ConsoleLoggerMock! + var mockNotificationCenter: MockNotificationCenter! + + override func setUp() { + super.setUp() + mockRequester = MockNotifyWatchSubscriptionsRequester() + mockLogger = ConsoleLoggerMock() + mockNotificationCenter = MockNotificationCenter() + sut = SubscriptionWatcher(notifyWatchSubscriptionsRequester: mockRequester, logger: mockLogger, notificationCenter: mockNotificationCenter) + sut.debounceInterval = 0.0001 + sut.start() + } + + override func tearDown() { + sut = nil + mockRequester = nil + mockLogger = nil + mockNotificationCenter = nil + super.tearDown() + } + + func testWatchSubscriptions() { + let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called") + + mockRequester.onWatchSubscriptions = { + expectation.fulfill() + } + + sut.watchSubscriptions() + + wait(for: [expectation], timeout: 0.5) + } + + + func testWatchAppLifecycleReactsToEnterForegroundNotification() { + let setupExpectation = XCTestExpectation(description: "Expect setupTimer to be called on app enter foreground") + let watchSubscriptionsExpectation = XCTestExpectation(description: "Expect watchSubscriptions to be called on app enter foreground") + + sut.onSetupTimer = { + setupExpectation.fulfill() + } + + mockRequester.onWatchSubscriptions = { + watchSubscriptionsExpectation.fulfill() + } + + mockNotificationCenter.post(name: UIApplication.willEnterForegroundNotification) + + wait(for: [setupExpectation, watchSubscriptionsExpectation], timeout: 0.5) + } + + func testTimerTriggeringWatchSubscriptionsMultipleTimes() { + sut.timerInterval = 0.0001 + sut.setupTimer() + + let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called multiple times") + expectation.expectedFulfillmentCount = 3 + + mockRequester.onWatchSubscriptions = { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0.5) + } +} diff --git a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift index 05532fa8d..45b147637 100644 --- a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift +++ b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift @@ -8,17 +8,20 @@ final class ClientIdStorageTests: XCTestCase { var sut: ClientIdStorage! var keychain: KeychainStorageMock! + var defaults: RuntimeKeyValueStorage! override func setUp() { keychain = KeychainStorageMock() - sut = ClientIdStorage(keychain: keychain) + defaults = RuntimeKeyValueStorage() + sut = ClientIdStorage(defaults: defaults, keychain: keychain, logger: ConsoleLoggerMock()) } func testGetOrCreate() throws { XCTAssertThrowsError(try keychain.read(key: "com.walletconnect.iridium.client_id") as SigningPrivateKey) let saved = try sut.getOrCreateKeyPair() - XCTAssertEqual(saved, try keychain.read(key: "com.walletconnect.iridium.client_id")) + let storageId = saved.publicKey.rawRepresentation.sha256().toHexString() + XCTAssertEqual(saved, try keychain.read(key: storageId)) let restored = try sut.getOrCreateKeyPair() XCTAssertEqual(saved, restored) @@ -27,11 +30,51 @@ final class ClientIdStorageTests: XCTestCase { func testGetClientId() throws { let didKey = try DIDKey(did: "did:key:z6MkodHZwneVRShtaLf8JKYkxpDGp1vGZnpGmdBpX8M2exxH") + /// Initial state + XCTAssertThrowsError(try sut.getClientId()) + let privateKey = try SigningPrivateKey(rawRepresentation: didKey.rawData) - try keychain.add(privateKey, forKey: "com.walletconnect.iridium.client_id") + + defaults.set(privateKey.publicKey.rawRepresentation, forKey: "com.walletconnect.iridium.client_id.public") + + /// Private part not found + XCTAssertThrowsError(try sut.getClientId()) + + let storageId = privateKey.publicKey.rawRepresentation.sha256().toHexString() + try keychain.add(privateKey, forKey: storageId) let clientId = try sut.getClientId() let didPublicKey = DIDKey(rawData: privateKey.publicKey.rawRepresentation) + XCTAssertEqual(clientId, didPublicKey.did(variant: .ED25519)) } + + func testMigration() throws { + let defaults = RuntimeKeyValueStorage() + let keychain = KeychainStorageMock() + let clientId = SigningPrivateKey() + + try keychain.add(clientId, forKey: "com.walletconnect.iridium.client_id") + + // Migration on init + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychain, logger: ConsoleLoggerMock()) + + let publicPartData = defaults.data(forKey: "com.walletconnect.iridium.client_id.public")! + let publicPart = try SigningPublicKey(rawRepresentation: publicPartData) + + let privatePartStorageId = publicPart.rawRepresentation.sha256().toHexString() + let privatePart: SigningPrivateKey = try keychain.read(key: privatePartStorageId) + + XCTAssertEqual(publicPart, clientId.publicKey) + XCTAssertEqual(privatePart, clientId) + + let oldClientId: SigningPrivateKey? = try? keychain.read(key: "com.walletconnect.iridium.client_id") + XCTAssertNil(oldClientId) + + let restoredPrivatePart = try clientIdStorage.getOrCreateKeyPair() + XCTAssertEqual(restoredPrivatePart, clientId) + + let restoredPublicPart = try clientIdStorage.getClientId() + XCTAssertEqual(restoredPublicPart, DIDKey(rawData: clientId.publicKey.rawRepresentation).did(variant: .ED25519)) + } } diff --git a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift index 87d5ae4c3..cbd8d2878 100644 --- a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift +++ b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift @@ -11,8 +11,8 @@ final class EdDSASignerTests: XCTestCase { let keyRaw = Data(hex: "58e0254c211b858ef7896b00e3f36beeb13d568d47c6031c4218b87718061295") let signingKey = try! SigningPrivateKey(rawRepresentation: keyRaw) sut = EdDSASigner(signingKey) - let header = try! JWTHeader(alg: "EdDSA").encode() - let claims = try! RelayAuthPayload.Claims.stub().encode() + let header = try! JWTHeader(alg: "EdDSA").encode(jsonEncoder: .jwt) + let claims = try! RelayAuthPayload.Claims.stub().encode(jsonEncoder: .jwt) let signature = try! sut.sign(header: header, claims: claims) XCTAssertNotNil(signature) } diff --git a/Tests/RelayerTests/AuthTests/JWTTests.swift b/Tests/RelayerTests/AuthTests/JWTTests.swift index 40ad5be31..9e662ff72 100644 --- a/Tests/RelayerTests/AuthTests/JWTTests.swift +++ b/Tests/RelayerTests/AuthTests/JWTTests.swift @@ -4,15 +4,16 @@ import XCTest @testable import WalletConnectJWT final class JWTTests: XCTestCase { - let expectedJWT = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTY5MTAwOTcsImV4cCI6MTY1Njk5NjQ5NywiaXNzIjoiZGlkOmtleTp6Nk1rb2RIWnduZVZSU2h0YUxmOEpLWWt4cERHcDF2R1pucEdtZEJwWDhNMmV4eEgiLCJzdWIiOiJjNDc5ZmU1ZGM0NjRlNzcxZTc4YjE5M2QyMzlhNjViNThkMjc4Y2FkMWMzNGJmYjBiNTcxNmU1YmI1MTQ5MjhlIiwiYXVkIjoid3NzOi8vcmVsYXkud2FsbGV0Y29ubmVjdC5jb20ifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" + let expectedJWT = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ3c3M6Ly9yZWxheS53YWxsZXRjb25uZWN0LmNvbSIsImV4cCI6MTY1Njk5NjQ5NywiaWF0IjoxNjU2OTEwMDk3LCJpc3MiOiJkaWQ6a2V5Ono2TWtvZEhad25lVlJTaHRhTGY4SktZa3hwREdwMXZHWm5wR21kQnBYOE0yZXh4SCIsInN1YiI6ImM0NzlmZTVkYzQ2NGU3NzFlNzhiMTkzZDIzOWE2NWI1OGQyNzhjYWQxYzM0YmZiMGI1NzE2ZTViYjUxNDkyOGUifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" - func testJWTEncoding() { - var jwt = JWT(claims: RelayAuthPayload.Claims.stub()) + func testJWTEncoding() throws { let signer = EdDSASignerMock() signer.signature = "0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" - try! jwt.sign(using: signer) - let encoded = try! jwt.encoded() - XCTAssertEqual(expectedJWT, encoded) + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + let jwt = try JWT(claims: RelayAuthPayload.Claims.stub(), signer: signer, jsonEncoder: jsonEncoder) + XCTAssertEqual(expectedJWT, jwt.string) } func testBase64Encoding() throws { diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 4d581239e..a93d5a5d3 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -64,8 +64,10 @@ final class DispatcherTests: XCTestCase { webSocket = WebSocketMock() let webSocketFactory = WebSocketFactoryMock(webSocket: webSocket) networkMonitor = NetworkMonitoringMock() + let defaults = RuntimeKeyValueStorage() + let logger = ConsoleLoggerMock() let keychainStorageMock = DispatcherKeychainStorageMock() - let clientIdStorage = ClientIdStorage(keychain: keychainStorageMock) + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychainStorageMock, logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, url: "wss://relay.walletconnect.com" diff --git a/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift b/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift index 935043f72..29fe44452 100644 --- a/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift +++ b/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift @@ -2,10 +2,12 @@ import Foundation @testable import WalletConnectKMS final class KeyManagementServiceMock: KeyManagementServiceProtocol { + private(set) var privateKeys: [String: AgreementPrivateKey] = [:] private(set) var symmetricKeys: [String: SymmetricKey] = [:] private(set) var agreementKeys: [String: AgreementKeys] = [:] private(set) var publicKeys: [String: AgreementPublicKey] = [:] + private(set) var topics: [String: String] = [:] func getSymmetricKeyRepresentable(for topic: String) -> Data? { if let key = getAgreementSecret(for: topic)?.sharedKey { @@ -95,6 +97,18 @@ final class KeyManagementServiceMock: KeyManagementServiceProtocol { symmetricKeys = [:] agreementKeys = [:] } + + func setTopic(_ topic: String, for key: String) throws { + topics[key] = topic + } + + func getTopic(for key: String) -> String? { + return topics[key] + } + + func deleteTopic(for key: String) { + topics[key] = nil + } } extension KeyManagementServiceMock { diff --git a/Tests/Web3WalletTests/Mocks/SignClientMock.swift b/Tests/Web3WalletTests/Mocks/SignClientMock.swift index 3d5cca9fe..62ba27489 100644 --- a/Tests/Web3WalletTests/Mocks/SignClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/SignClientMock.swift @@ -59,6 +59,17 @@ final class SignClientMock: SignClientProtocol { .eraseToAnyPublisher() } + var sessionEventPublisher: AnyPublisher<(event: WalletConnectSign.Session.Event, sessionTopic: String, chainId: WalletConnectUtils.Blockchain?), Never> { + return Result.Publisher( + ( + WalletConnectSign.Session.Event(name: "chainChanged", data: AnyCodable("event_data")), + "topic", + Blockchain("eip155:1") + ) + ) + .eraseToAnyPublisher() + } + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { let sessionProposal = Session.Proposal( id: "", diff --git a/WalletConnectSwiftV2.podspec b/WalletConnectSwiftV2.podspec index c2f80eb8f..cc6ab4f3d 100644 --- a/WalletConnectSwiftV2.podspec +++ b/WalletConnectSwiftV2.podspec @@ -78,7 +78,7 @@ Pod::Spec.new do |spec| ss.source_files = 'Sources/Web3Wallet/**/*.{h,m,swift}' ss.dependency 'WalletConnectSwiftV2/WalletConnectSign' ss.dependency 'WalletConnectSwiftV2/WalletConnectAuth' - ss.dependency 'WalletConnectSwiftV2/WalletConnectEcho' + ss.dependency 'WalletConnectSwiftV2/WalletConnectPush' ss.dependency 'WalletConnectSwiftV2/WalletConnectVerify' end @@ -130,8 +130,8 @@ Pod::Spec.new do |spec| ss.dependency 'WalletConnectSwiftV2/WalletConnectJWT' end - spec.subspec 'WalletConnectEcho' do |ss| - ss.source_files = 'Sources/WalletConnectEcho/**/*.{h,m,swift}' + spec.subspec 'WalletConnectPush' do |ss| + ss.source_files = 'Sources/WalletConnectPush/**/*.{h,m,swift}' ss.dependency 'WalletConnectSwiftV2/WalletConnectNetworking' ss.dependency 'WalletConnectSwiftV2/WalletConnectJWT' end diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ef829c945..114bce38f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -112,10 +112,7 @@ platform :ios do distribute_external: true, notify_external_testers: true, skip_waiting_for_build_processing: false, - groups: [ - "WalletConnect", - "WalletConnect Users" - ] + groups: ["WalletConnect Users"] ) clean_build_artifacts() end diff --git a/run_tests.sh b/run_tests.sh index eb906df0e..ea5cad206 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -84,6 +84,7 @@ else update_xctestrun --key "PROJECT_ID" --value "$PROJECT_ID" --target "$XCTESTRUN" update_xctestrun --key "GM_DAPP_PROJECT_ID" --value "$GM_DAPP_PROJECT_ID" --target "$XCTESTRUN" update_xctestrun --key "GM_DAPP_PROJECT_SECRET" --value "$GM_DAPP_PROJECT_SECRET" --target "$XCTESTRUN" + update_xctestrun --key "GM_DAPP_HOST" --value "$GM_DAPP_HOST" --target "$XCTESTRUN" update_xctestrun --key "CAST_HOST" --value "$CAST_HOST" --target "$XCTESTRUN" update_xctestrun --key "JS_CLIENT_API_HOST" --value "$JS_CLIENT_API_HOST" --target "$XCTESTRUN"