diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000..0e3715428 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,38 @@ +name: 'build' +description: 'Compiles and stores artifacts for further use' +inputs: + relay-endpoint: + description: 'The endpoint of the relay e.g. relay.walletconnect.com' + required: false + default: 'relay.walletconnect.com' + project-id: + description: 'WalletConnect project id' + required: true + +runs: + using: "composite" + steps: + - uses: actions/checkout@v3 + + - uses: actions/cache@v3 + with: + path: | + **/SourcePackagesCache + DerivedDataCache + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Build for testing + shell: bash + run: make build_all RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} + + - name: Tar DerivedDataCache + shell: bash + run: test -d "DerivedDataCache" && tar cfPp products.tar --format posix DerivedDataCache/Build + + - uses: actions/cache/save@v3 + with: + path: | + products.tar + key: ${{ runner.os }}-deriveddata-${{ github.ref }}-${{ github.sha }} \ No newline at end of file diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml deleted file mode 100644 index cf03c7a7d..000000000 --- a/.github/actions/ci/action.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: 'ci' -description: 'Executes Swift specific CI steps' -inputs: - type: - description: 'The type of CI step to run' - required: true - relay-endpoint: - description: 'The endpoint of the relay e.g. relay.walletconnect.com' - required: false - default: 'relay.walletconnect.com' - project-id: - description: 'WalletConnect project id' - required: true - -runs: - using: "composite" - steps: - # Package builds - - name: Run tests - if: inputs.type == 'unit-tests' - shell: bash - run: make unit_tests - - # Integration tests - - name: Run integration tests - if: inputs.type == 'integration-tests' - shell: bash - env: - RELAY_ENDPOINT: ${{ inputs.relay-endpoint }} - PROJECT_ID: ${{ inputs.project-id }} - run: make integration_tests RELAY_HOST=$RELAY_ENDPOINT PROJECT_ID=$PROJECT_ID - - # Relay Integration tests - - name: Run integration tests - if: inputs.type == 'relay-tests' - shell: bash - env: - RELAY_ENDPOINT: ${{ inputs.relay-endpoint }} - PROJECT_ID: ${{ inputs.project-id }} - run: make relay_tests RELAY_HOST=$RELAY_ENDPOINT PROJECT_ID=$PROJECT_ID - - # Smoke tests - - name: Run smoke tests - if: inputs.type == 'smoke-tests' - shell: bash - env: - RELAY_ENDPOINT: ${{ inputs.relay-endpoint }} - PROJECT_ID: ${{ inputs.project-id }} - run: make smoke_tests RELAY_HOST=$RELAY_ENDPOINT PROJECT_ID=$PROJECT_ID - - - # Wallet build - - name: Build Example Wallet - if: inputs.type == 'build-example-wallet' - shell: bash - run: make build_wallet - - # DApp build - - name: Build Example Dapp - if: inputs.type == 'build-example-dapp' - shell: bash - run: make build_dapp - - # UI tests - - name: UI Tests - if: inputs.type == 'ui-tests' - shell: bash - run: make ui_tests - continue-on-error: true diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml new file mode 100644 index 000000000..d3abfb8cd --- /dev/null +++ b/.github/actions/run_tests_without_building/action.yml @@ -0,0 +1,73 @@ +name: 'run_tests_without_building' +description: 'Executes specific Swift tests using prebuilt artifacts from build_artifacts.yml workflow from main branch' +inputs: + type: + description: 'The type of CI step to run' + required: true + relay-endpoint: + description: 'The endpoint of the relay e.g. relay.walletconnect.com' + required: false + default: 'relay.walletconnect.com' + project-id: + description: 'WalletConnect project id' + required: true + +runs: + using: "composite" + steps: + - name: Download artifact + id: download-artifact + uses: dawidd6/action-download-artifact@v2 + with: + name: main-derivedData + workflow: build_artifacts.yml + repo: 'WalletConnect/WalletConnectSwiftV2' + if_no_artifact_found: warn + + - name: Untar DerivedDataCache + shell: bash + run: test -f products.tar && tar xPpf products.tar || echo "No artifacts to untar" + + # Package Unit tests + - name: Run tests + if: inputs.type == 'unit-tests' + shell: bash + run: make unit_tests + + # Integration tests + - 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 }} + + # Relay Integration tests + - name: Run Relay integration tests + if: inputs.type == 'relay-tests' + shell: bash + run: make relay_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} + + # Smoke tests + - name: Run smoke tests + if: inputs.type == 'smoke-tests' + shell: bash + run: make smoke_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: success() || failure() + with: + check_name: ${{ inputs.type }} junit report + report_paths: 'test_results/report.junit' + + - name: Zip test artifacts + if: always() + shell: bash + run: test -d "test_results" && zip artifacts.zip -r ./test_results || echo "Nothing to zip" + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.type }} test_results + path: ./artifacts.zip + if-no-files-found: warn \ No newline at end of file diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml new file mode 100644 index 000000000..b51a09e9f --- /dev/null +++ b/.github/workflows/build_artifacts.yml @@ -0,0 +1,50 @@ +name: build_artifacts + +on: + workflow_dispatch: + inputs: + relay-endpoint: + description: 'The endpoint of the relay e.g. relay.walletconnect.com' + required: false + default: 'relay.walletconnect.com' + project-id: + description: 'WalletConnect project id' + required: true + push: + branches: [ main ] + +jobs: + build: + runs-on: macos-12 + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/cache@v3 + with: + path: | + **/SourcePackagesCache + DerivedDataCache + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Build for testing on workflow_dispatch + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: bash + run: make build_all RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} + + - name: Build for testing on push + if: ${{ github.event_name == 'push' }} + shell: bash + run: make build_all RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} + + - name: Tar DerivedDataCache + shell: bash + run: test -d "DerivedDataCache" && tar cfPp products.tar --format posix DerivedDataCache/Build + + - uses: actions/upload-artifact@v3 + with: + name: main-derivedData + path: products.tar \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8726a1632..fd3aa5788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,44 +5,84 @@ on: branches: [ main, develop ] concurrency: - # Support push/pr as event types with different behaviors each: - # 1. push: queue up builds by branch - # 2. pr: only allow one run per PR group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref_name }} - # If there is already a workflow running for the same pull request, cancel it - # For non-PR triggers queue up builds cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - build: + prepare: runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/actions/build + with: + project-id: ${{ secrets.PROJECT_ID }} + + test: + needs: prepare + runs-on: macos-12 + timeout-minutes: 15 strategy: + fail-fast: false matrix: - test-type: [unit-tests, integration-tests, build-example-wallet, build-example-dapp, relay-tests] + type: [integration-tests, relay-tests, unit-tests] steps: - - uses: actions/checkout@v2 - - - name: Setup Xcode Version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: 14.1 + - uses: actions/checkout@v3 - - uses: actions/cache@v2 + - uses: actions/cache/restore@v3 with: path: | - .build - SourcePackagesCache - DerivedDataCache - key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + products.tar + key: ${{ runner.os }}-deriveddata-${{ github.ref }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-spm- + ${{ runner.os }}-deriveddata-${{ github.ref }}- + + - name: Untar DerivedDataCache + shell: bash + run: test -f products.tar && tar xPpf products.tar || echo "No artifacts to untar" + + # Package Unit tests + - name: Run tests + if: matrix.type == 'unit-tests' + shell: bash + run: make unit_tests - - name: Resolve Dependencies + # Integration tests + - name: Run integration tests + if: matrix.type == 'integration-tests' shell: bash - run: make resolve_packages + run: make integration_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} - - uses: ./.github/actions/ci + # Relay Integration tests + - name: Run Relay integration tests + if: matrix.type == 'relay-tests' + shell: bash + run: make relay_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} + + # Smoke tests + - name: Run smoke tests + if: matrix.type == 'smoke-tests' + shell: bash + run: make smoke_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: success() || failure() with: - type: ${{ matrix.test-type }} - project-id: ${{ secrets.PROJECT_ID }} + check_name: ${{ matrix.type }} junit report + report_paths: 'test_results/report.junit' + + - name: Zip test artifacts + if: always() + shell: bash + run: test -d "test_results" && zip artifacts.zip -r ./test_results || echo "Nothing to zip" + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.type }} test_results + path: ./artifacts.zip + if-no-files-found: warn + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c30113723..9a58e1a2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,9 @@ jobs: runs-on: macos-12 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - name: Setup Xcode Version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: 14.1 - - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | .build diff --git a/.gitignore b/.gitignore index cdf6e24d3..172334855 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ DerivedDataCache # Artifacts *.ipa -*.zip \ No newline at end of file +*.zip +test_results/ diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme new file mode 100644 index 000000000..bcf1ba2ab --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/DApp/Sign/Connect/ConnectView.swift b/Example/DApp/Sign/Connect/ConnectView.swift index c1c075504..7004411a0 100644 --- a/Example/DApp/Sign/Connect/ConnectView.swift +++ b/Example/DApp/Sign/Connect/ConnectView.swift @@ -31,10 +31,18 @@ final class ConnectView: UIView { return button }() + let invisibleUriLabel: UILabel = { + let label = UILabel(frame: CGRect(origin: .zero, size: .init(width: 1, height: 1))) + label.numberOfLines = 0 + label.textColor = .clear + return label + }() + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .systemBackground addSubview(qrCodeView) + addSubview(invisibleUriLabel) addSubview(copyButton) addSubview(connectWalletButton) addSubview(tableView) diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 9784b062c..02c9020f7 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -34,6 +34,8 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie self.connectView.copyButton.isHidden = false } } + + connectView.invisibleUriLabel.text = uri.absoluteString connectView.copyButton.addTarget(self, action: #selector(copyURI), for: .touchUpInside) connectView.connectWalletButton.addTarget(self, action: #selector(connectWithExampleWallet), for: .touchUpInside) connectView.tableView.dataSource = self diff --git a/Example/EchoUITests/Engine/App.swift b/Example/EchoUITests/Engine/App.swift new file mode 100644 index 000000000..e273bbabc --- /dev/null +++ b/Example/EchoUITests/Engine/App.swift @@ -0,0 +1,23 @@ +import Foundation +import XCTest + +enum App { + case dapp + case wallet + case springboard + + var instance: XCUIApplication { + return XCUIApplication(bundleIdentifier: bundleID) + } + + private var bundleID: String { + switch self { + case .dapp: + return "com.walletconnect.dapp" + case .wallet: + return "com.walletconnect.walletapp" + case .springboard: + return "com.apple.springboard" + } + } +} diff --git a/Example/EchoUITests/Engine/DAppEngine.swift b/Example/EchoUITests/Engine/DAppEngine.swift new file mode 100644 index 000000000..5711ccce8 --- /dev/null +++ b/Example/EchoUITests/Engine/DAppEngine.swift @@ -0,0 +1,35 @@ +import Foundation +import XCTest + +struct DAppEngine { + + var instance: XCUIApplication { + return App.dapp.instance + } + + // Main screen + + var connectButton: XCUIElement { + instance.buttons["Connect"] + } + + // Accounts screen + + var accountRow: XCUIElement { + instance.tables.cells.containing("0x").firstMatch + } + + var methodRow: XCUIElement { + instance.tables.cells.firstMatch + } + + // Pairing screen + + var newPairingButton: XCUIElement { + instance.buttons["New Pairing"] + } + + var copyURIButton: XCUIElement { + instance.buttons["Copy"] + } +} diff --git a/Example/EchoUITests/Engine/Engine.swift b/Example/EchoUITests/Engine/Engine.swift new file mode 100644 index 000000000..c164f227a --- /dev/null +++ b/Example/EchoUITests/Engine/Engine.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +struct Engine { + let routing = RoutingEngine() + let dapp = DAppEngine() + let wallet = WalletEngine() +} diff --git a/Example/EchoUITests/Engine/RoutingEngine.swift b/Example/EchoUITests/Engine/RoutingEngine.swift new file mode 100644 index 000000000..6de1874aa --- /dev/null +++ b/Example/EchoUITests/Engine/RoutingEngine.swift @@ -0,0 +1,32 @@ +import Foundation +import XCTest + +struct RoutingEngine { + + var springboard: XCUIApplication { + return App.springboard.instance + } + + func launch(app: App, clean: Bool) { + app.instance.terminate() + + if clean { + let app = app.instance + app.launchArguments = ["-cleanInstall", "-disableAnimations"] + app.launch() + } else { + let app = app.instance + app.launch() + } + } + + func activate(app: App) { + let app = app.instance + app.activate() + app.wait(until: \.exists) + } + + func wait(for interval: TimeInterval) { + Thread.sleep(forTimeInterval: interval) + } +} diff --git a/Example/EchoUITests/Engine/WalletEngine.swift b/Example/EchoUITests/Engine/WalletEngine.swift new file mode 100644 index 000000000..c45d327a8 --- /dev/null +++ b/Example/EchoUITests/Engine/WalletEngine.swift @@ -0,0 +1,39 @@ +import Foundation +import XCTest + +struct WalletEngine { + + var instance: XCUIApplication { + return App.wallet.instance + } + + // Onboarding + + var getStartedButton: XCUIElement { + instance.buttons["Get Started"] + } + + // MainScreen + + var pasteURIButton: XCUIElement { + instance.buttons["copy"] + } + + var alertUriTextField: XCUIElement { + instance.textFields["wc://a13aef..."] + } + + var alertConnectButton: XCUIElement { + instance.buttons["Connect"] + } + + var sessionRow: XCUIElement { + instance.staticTexts["Swift Dapp"] + } + + // Proposal + + var allowButton: XCUIElement { + instance.buttons["Allow"] + } +} diff --git a/Example/EchoUITests/Extensions/XCTestCase.swift b/Example/EchoUITests/Extensions/XCTestCase.swift new file mode 100644 index 000000000..f2698a34b --- /dev/null +++ b/Example/EchoUITests/Extensions/XCTestCase.swift @@ -0,0 +1,18 @@ +import Foundation +import XCTest + +extension XCTestCase { + + func allowPushNotificationsIfNeeded(app: XCUIApplication) { + let pnPermission = addUIInterruptionMonitor(withDescription: "Push Notification Monitor") { alerts -> Bool in + if alerts.buttons["Allow"].exists { + alerts.buttons["Allow"].tap() + } + + return true + } + app.swipeUp() + + self.removeUIInterruptionMonitor(pnPermission) + } +} diff --git a/Example/EchoUITests/Extensions/XCUIElement.swift b/Example/EchoUITests/Extensions/XCUIElement.swift new file mode 100644 index 000000000..514a23242 --- /dev/null +++ b/Example/EchoUITests/Extensions/XCUIElement.swift @@ -0,0 +1,74 @@ +import Foundation +import XCTest + +extension XCUIElement { + + static let waitTimeout: TimeInterval = 15 + + @discardableResult + func wait( + until expression: @escaping (XCUIElement) -> Bool, + timeout: TimeInterval = waitTimeout, + message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line + ) -> Self { + if expression(self) { + return self + } + + let predicate = NSPredicate { _, _ in + expression(self) + } + + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: nil) + + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + + if result != .completed { + XCTFail( + message().isEmpty ? "expectation not matched after waiting" : message(), + file: file, + line: line + ) + } + + return self + } + + @discardableResult + func wait( + until keyPath: KeyPath, + matches match: Value, + timeout: TimeInterval = waitTimeout, + message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line + ) -> Self { + wait( + until: { $0[keyPath: keyPath] == match }, + timeout: timeout, + message: message(), + file: file, + line: line + ) + } + + @discardableResult + func wait( + until keyPath: KeyPath, + timeout: TimeInterval = waitTimeout, + message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line + ) -> Self { + wait( + until: keyPath, + matches: true, + timeout: timeout, + message: message(), + file: file, + line: line + ) + } +} diff --git a/Example/EchoUITests/Extensions/XCUIElementQuery.swift b/Example/EchoUITests/Extensions/XCUIElementQuery.swift new file mode 100644 index 000000000..a92ed3a08 --- /dev/null +++ b/Example/EchoUITests/Extensions/XCUIElementQuery.swift @@ -0,0 +1,11 @@ +import Foundation +import XCTest + +extension XCUIElementQuery { + + func containing(_ text: String) -> XCUIElementQuery { + let predicate = NSPredicate(format: "label CONTAINS[c] %@", text) + let elementQuery = self.containing(predicate) + return elementQuery + } +} diff --git a/Example/EchoUITests/Tests/PushNotificationTests.swift b/Example/EchoUITests/Tests/PushNotificationTests.swift new file mode 100644 index 000000000..c9db0aeca --- /dev/null +++ b/Example/EchoUITests/Tests/PushNotificationTests.swift @@ -0,0 +1,57 @@ +import XCTest + +class PushNotificationTests: XCTestCase { + + private var engine: Engine! + + override func setUp() { + super.setUp() + engine = Engine() + engine.routing.launch(app: .wallet, clean: true) + engine.routing.launch(app: .dapp, clean: true) + } + + func testPushNotification() { + + // Initiate connection & copy URI from dApp + engine.routing.activate(app: .dapp) + engine.dapp.connectButton.wait(until: \.exists).tap() + engine.dapp.newPairingButton.wait(until: \.exists).tap() + + // Relies on existence of invisible label with uri in Dapp + let uri = engine.dapp.instance.staticTexts.containing("wc:").firstMatch.label + + engine.dapp.copyURIButton.wait(until: \.exists).tap() + + // Paste URI into Wallet & and allow connect + engine.routing.activate(app: .wallet) + + allowPushNotificationsIfNeeded(app: engine.wallet.instance) + + engine.wallet.getStartedButton.wait(until: \.exists).tap() + engine.wallet.pasteURIButton.wait(until: \.exists).tap() + + engine.wallet.alertUriTextField.wait(until: \.exists).tap() + engine.wallet.alertUriTextField.typeText(uri) + engine.wallet.alertConnectButton.wait(until: \.exists).tap() + + // Allow session + engine.wallet.allowButton.wait(until: \.exists, timeout: 15, message: "No session dialog appeared").tap() + + // Trigger PN + engine.routing.activate(app: .dapp) + engine.dapp.accountRow.wait(until: \.exists, timeout: 15).tap() + engine.dapp.methodRow.wait(until: \.exists).tap() + + // Launch springboard + engine.routing.activate(app: .springboard) + + // Assert notification + let notification = engine.routing.springboard.otherElements.descendants(matching: .any)["NotificationShortLookView"] + notification + .wait(until: \.exists, timeout: 15) + .tap() + + engine.wallet.instance.wait(until: \.exists) + } +} diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index f16df11a9..cf8c21e00 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ 84474A0129B9EB74005F520B /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 84474A0029B9EB74005F520B /* Starscream */; }; 84474A0229B9ECA2005F520B /* DefaultSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AEF2877F73000094373 /* DefaultSocketFactory.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 */; }; @@ -31,7 +35,6 @@ 847BD1E8298A806800076C90 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1E3298A806800076C90 /* NotificationsView.swift */; }; 847BD1EB298A87AB00076C90 /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */; }; 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 847CF3AE28E3141700F1D760 /* WalletConnectPush */; settings = {ATTRIBUTES = (Required, ); }; }; - 8485617F295307C20064877B /* PushNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8485617E295307C20064877B /* PushNotificationTests.swift */; }; 849D7A93292E2169006A2BD4 /* PushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* PushTests.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154D2991099000FAD54E /* BuildConfiguration.swift */; }; @@ -264,6 +267,15 @@ 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 */; }; + 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 */; }; + CF1A594929E5876600AAC16B /* WalletEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A594029E5876600AAC16B /* WalletEngine.swift */; }; + CF1A594B29E5876600AAC16B /* DAppEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A594229E5876600AAC16B /* DAppEngine.swift */; }; + CF1A594C29E5876600AAC16B /* RoutingEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A594329E5876600AAC16B /* RoutingEngine.swift */; }; + CF1A594D29E5876600AAC16B /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A594429E5876600AAC16B /* App.swift */; }; + CF6704DF29E59DDC003326A4 /* XCUIElementQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6704DE29E59DDC003326A4 /* XCUIElementQuery.swift */; }; + CF6704E129E5A014003326A4 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6704E029E5A014003326A4 /* XCTestCase.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -281,6 +293,27 @@ remoteGlobalIDString = 84CE641B27981DED00142511; remoteInfo = DApp; }; + CF11913F29E5D86F000D4538 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84CE641B27981DED00142511; + remoteInfo = DApp; + }; + CF11914129E5D873000D4538 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C56EE21A293F55ED004840D1; + remoteInfo = WalletApp; + }; + CF12A92229E847D600B42F2A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84E6B84629787A8000428BAF; + remoteInfo = PNDecryptionService; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -309,6 +342,9 @@ 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 = ""; }; @@ -323,7 +359,6 @@ 847BD1E2298A806800076C90 /* NotificationsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsInteractor.swift; sourceTree = ""; }; 847BD1E3298A806800076C90 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModel.swift; sourceTree = ""; }; - 8485617E295307C20064877B /* PushNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationTests.swift; sourceTree = ""; }; 849A4F18298281E300E61ACE /* WalletAppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletAppRelease.entitlements; sourceTree = ""; }; 849A4F19298281F100E61ACE /* PNDecryptionServiceRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionServiceRelease.entitlements; sourceTree = ""; }; 849D7A92292E2169006A2BD4 /* PushTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTests.swift; sourceTree = ""; }; @@ -536,6 +571,17 @@ 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 = ""; }; + 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 = ""; }; + CF1A593F29E5876600AAC16B /* Engine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Engine.swift; sourceTree = ""; }; + CF1A594029E5876600AAC16B /* WalletEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletEngine.swift; sourceTree = ""; }; + CF1A594229E5876600AAC16B /* DAppEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppEngine.swift; sourceTree = ""; }; + CF1A594329E5876600AAC16B /* RoutingEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoutingEngine.swift; sourceTree = ""; }; + CF1A594429E5876600AAC16B /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + CF6704DE29E59DDC003326A4 /* XCUIElementQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElementQuery.swift; sourceTree = ""; }; + CF6704E029E5A014003326A4 /* XCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; + CF79389D29EDD9DC00441B4F /* RelayIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = RelayIntegrationTests.xctestplan; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -608,6 +654,7 @@ files = ( C56EE27D293F56F8004840D1 /* WalletConnectChat in Frameworks */, C5133A78294125CC00A8314C /* Web3 in Frameworks */, + 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */, C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */, C55D349929630D440004314A /* Web3Wallet in Frameworks */, 84E6B85429787AAE00428BAF /* WalletConnectPush in Frameworks */, @@ -616,12 +663,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CF1A592D29E5873D00AAC16B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 764E1D3326F8D3FC00A1FB15 = { isa = PBXGroup; children = ( + CF79389D29EDD9DC00441B4F /* RelayIntegrationTests.xctestplan */, 845AA7D929BA1EBA00F33739 /* IntegrationTests.xctestplan */, 845AA7DC29BB424800F33739 /* SmokeTests.xctestplan */, A5A8E479293A1C4400FEB97D /* Shared */, @@ -634,6 +689,7 @@ C56EE21C293F55ED004840D1 /* WalletApp */, 84E6B84829787A8000428BAF /* PNDecryptionService */, 844749F429B9E5B9005F520B /* RelayIntegrationTests */, + CF1A593129E5873D00AAC16B /* EchoUITests */, 764E1D3D26F8D3FC00A1FB15 /* Products */, 764E1D5326F8DAC800A1FB15 /* Frameworks */, 764E1D5626F8DB6000A1FB15 /* WalletConnectSwiftV2 */, @@ -650,6 +706,7 @@ C56EE21B293F55ED004840D1 /* WalletApp.app */, 84E6B84729787A8000428BAF /* PNDecryptionService.appex */, 844749F329B9E5B9005F520B /* RelayIntegrationTests.xctest */, + CF1A593029E5873D00AAC16B /* EchoUITests.xctest */, ); name = Products; sourceTree = ""; @@ -681,6 +738,16 @@ 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 = ( @@ -1314,7 +1381,6 @@ isa = PBXGroup; children = ( A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */, - 8485617E295307C20064877B /* PushNotificationTests.swift */, ); path = Regression; sourceTree = ""; @@ -1391,6 +1457,7 @@ C56EE229293F5668004840D1 /* Wallet */ = { isa = PBXGroup; children = ( + 84536D6C29EEAE0D008EA8DB /* Web3Inbox */, 847BD1DB2989493F00076C90 /* Main */, C55D3477295DD4AA0004314A /* Welcome */, C55D3474295DCB850004314A /* AuthRequest */, @@ -1575,6 +1642,46 @@ path = ConnectionDetails; sourceTree = ""; }; + CF1A593129E5873D00AAC16B /* EchoUITests */ = { + isa = PBXGroup; + children = ( + CF1A593E29E5876600AAC16B /* Engine */, + CF1A593929E5876600AAC16B /* Extensions */, + CF1A593B29E5876600AAC16B /* Tests */, + ); + path = EchoUITests; + sourceTree = ""; + }; + CF1A593929E5876600AAC16B /* Extensions */ = { + isa = PBXGroup; + children = ( + CF1A593A29E5876600AAC16B /* XCUIElement.swift */, + CF6704DE29E59DDC003326A4 /* XCUIElementQuery.swift */, + CF6704E029E5A014003326A4 /* XCTestCase.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + CF1A593B29E5876600AAC16B /* Tests */ = { + isa = PBXGroup; + children = ( + CF1A593C29E5876600AAC16B /* PushNotificationTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + CF1A593E29E5876600AAC16B /* Engine */ = { + isa = PBXGroup; + children = ( + CF1A593F29E5876600AAC16B /* Engine.swift */, + CF1A594029E5876600AAC16B /* WalletEngine.swift */, + CF1A594229E5876600AAC16B /* DAppEngine.swift */, + CF1A594329E5876600AAC16B /* RoutingEngine.swift */, + CF1A594429E5876600AAC16B /* App.swift */, + ); + path = Engine; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1735,18 +1842,39 @@ C55D349829630D440004314A /* Web3Wallet */, C5B2F7042970573D000DBA0E /* SolanaSwift */, 84E6B85329787AAE00428BAF /* WalletConnectPush */, + 84536D7329EEBCF0008EA8DB /* Web3Inbox */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; productType = "com.apple.product-type.application"; }; + CF1A592F29E5873D00AAC16B /* EchoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CF1A593629E5873D00AAC16B /* Build configuration list for PBXNativeTarget "EchoUITests" */; + buildPhases = ( + CF1A592C29E5873D00AAC16B /* Sources */, + CF1A592D29E5873D00AAC16B /* Frameworks */, + CF1A592E29E5873D00AAC16B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CF12A92329E847D600B42F2A /* PBXTargetDependency */, + CF11914229E5D873000D4538 /* PBXTargetDependency */, + CF11914029E5D86F000D4538 /* PBXTargetDependency */, + ); + name = EchoUITests; + productName = EchoUITests; + productReference = CF1A593029E5873D00AAC16B /* EchoUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 764E1D3426F8D3FC00A1FB15 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1250; TargetAttributes = { 844749F229B9E5B9005F520B = { @@ -1770,6 +1898,9 @@ C56EE21A293F55ED004840D1 = { CreatedOnToolsVersion = 14.1; }; + CF1A592F29E5873D00AAC16B = { + CreatedOnToolsVersion = 14.3; + }; }; }; buildConfigurationList = 764E1D3726F8D3FC00A1FB15 /* Build configuration list for PBXProject "ExampleApp" */; @@ -1798,6 +1929,7 @@ C56EE21A293F55ED004840D1 /* WalletApp */, 84E6B84629787A8000428BAF /* PNDecryptionService */, 844749F229B9E5B9005F520B /* RelayIntegrationTests */, + CF1A592F29E5873D00AAC16B /* EchoUITests */, ); }; /* End PBXProject section */ @@ -1858,6 +1990,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CF1A592E29E5873D00AAC16B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1990,7 +2129,6 @@ buildActionMask = 2147483647; files = ( A5E22D202840C8C700E36487 /* DAppEngine.swift in Sources */, - 8485617F295307C20064877B /* PushNotificationTests.swift in Sources */, A5E22D1E2840C8BF00E36487 /* RoutingEngine.swift in Sources */, A5E22D222840C8D300E36487 /* WalletEngine.swift in Sources */, A5E22D1C2840C85D00E36487 /* App.swift in Sources */, @@ -2047,6 +2185,8 @@ 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 */, @@ -2074,6 +2214,7 @@ 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 */, @@ -2132,6 +2273,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CF1A592C29E5873D00AAC16B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CF1A594D29E5876600AAC16B /* App.swift in Sources */, + CF1A594B29E5876600AAC16B /* DAppEngine.swift in Sources */, + CF6704DF29E59DDC003326A4 /* XCUIElementQuery.swift in Sources */, + CF1A594629E5876600AAC16B /* PushNotificationTests.swift in Sources */, + CF1A594829E5876600AAC16B /* Engine.swift in Sources */, + CF1A594529E5876600AAC16B /* XCUIElement.swift in Sources */, + CF1A594C29E5876600AAC16B /* RoutingEngine.swift in Sources */, + CF6704E129E5A014003326A4 /* XCTestCase.swift in Sources */, + CF1A594929E5876600AAC16B /* WalletEngine.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2145,6 +2302,21 @@ target = 84CE641B27981DED00142511 /* DApp */; targetProxy = A5A4FC7D2840C5D400BBEC1E /* PBXContainerItemProxy */; }; + CF11914029E5D86F000D4538 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84CE641B27981DED00142511 /* DApp */; + targetProxy = CF11913F29E5D86F000D4538 /* PBXContainerItemProxy */; + }; + CF11914229E5D873000D4538 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C56EE21A293F55ED004840D1 /* WalletApp */; + targetProxy = CF11914129E5D873000D4538 /* PBXContainerItemProxy */; + }; + CF12A92329E847D600B42F2A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84E6B84629787A8000428BAF /* PNDecryptionService */; + targetProxy = CF12A92229E847D600B42F2A /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -2580,6 +2752,7 @@ ); GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.IntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -2613,11 +2786,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = WalletApp/WalletApp.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WalletApp/Other/Info.plist; @@ -2636,7 +2808,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.walletconnect.walletapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2682,6 +2853,42 @@ }; name = Release; }; + CF1A593729E5873D00AAC16B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.EchoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CF1A593829E5873D00AAC16B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.EchoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2757,6 +2964,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CF1A593629E5873D00AAC16B /* Build configuration list for PBXNativeTarget "EchoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CF1A593729E5873D00AAC16B /* Debug */, + CF1A593829E5873D00AAC16B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -2808,6 +3024,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnect; }; + 84536D7329EEBCF0008EA8DB /* Web3Inbox */ = { + isa = XCSwiftPackageProductDependency; + productName = Web3Inbox; + }; 847CF3AE28E3141700F1D760 /* WalletConnectPush */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectPush; diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme new file mode 100644 index 000000000..4d2bf2723 --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/EchoUITests.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/EchoUITests.xcscheme new file mode 100644 index 000000000..40fa61364 --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/EchoUITests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/IntegrationTests/Push/PushTests.swift b/Example/IntegrationTests/Push/PushTests.swift index dc83c0042..3be34d4f2 100644 --- a/Example/IntegrationTests/Push/PushTests.swift +++ b/Example/IntegrationTests/Push/PushTests.swift @@ -237,7 +237,50 @@ final class PushTests: XCTestCase { wait(for: [expectation], timeout: InputConfig.defaultTimeout) } - private func sign(_ message: String) -> SigningResult { + // Push Subscribe + func testWalletCreatesSubscription() async { + let expectation = expectation(description: "expects to create push subscription") + let metadata = AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []) + try! await walletPushClient.subscribe(metadata: metadata, account: Account.stub(), onSign: sign) + walletPushClient.subscriptionsPublisher + .first() + .sink { [unowned self] subscriptions in + XCTAssertNotNil(subscriptions.first) + Task { try! await walletPushClient.deleteSubscription(topic: subscriptions.first!.topic) } + expectation.fulfill() + }.store(in: &publishers) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testWalletCreatesAndUpdatesSubscription() async { + let expectation = expectation(description: "expects to create and update push subscription") + let metadata = AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []) + let updateScope: Set = [NotificationScope.alerts] + try! await walletPushClient.subscribe(metadata: metadata, account: Account.stub(), onSign: sign) + walletPushClient.subscriptionsPublisher + .first() + .sink { [unowned self] subscriptions in + Task { try! await walletPushClient.update(topic: subscriptions.first!.topic, scope: updateScope) } + } + .store(in: &publishers) + + walletPushClient.updateSubscriptionPublisher + .sink { [unowned self] result in + guard case .success(let subscription) = result else { XCTFail(); return } + let updatedScope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) + XCTAssertEqual(updatedScope, updateScope) + Task { try! await walletPushClient.deleteSubscription(topic: subscription.topic) } + expectation.fulfill() + }.store(in: &publishers) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + +} + + +private extension PushTests { + func sign(_ message: String) -> SigningResult { let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) return .signed(try! signer.sign(message: message, privateKey: privateKey, type: .eip191)) diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index bc71628cf..583cd886a 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -164,7 +164,7 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces)! try await wallet.client.pair(uri: uri) - wait(for: [expectation], timeout: .infinity) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionRequest() async throws { diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift index db9f5d870..18bc68107 100644 --- a/Example/IntegrationTests/Stubs/PushMessage.swift +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -3,6 +3,6 @@ import WalletConnectPush extension PushMessage { static func stub() -> PushMessage { - return PushMessage(title: "test_push_message", body: "", icon: "", url: "") + return PushMessage(title: "test_push_message", body: "", icon: "", url: "", type: "") } } diff --git a/Example/RelayIntegrationTests.xctestplan b/Example/RelayIntegrationTests.xctestplan new file mode 100644 index 000000000..b4026d55e --- /dev/null +++ b/Example/RelayIntegrationTests.xctestplan @@ -0,0 +1,41 @@ +{ + "configurations" : [ + { + "id" : "3D7BF967-0C62-49DD-ABA1-BDDEE678ED85", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "environmentVariableEntries" : [ + { + "key" : "RELAY_HOST", + "value" : "$(RELAY_HOST)" + }, + { + "key" : "PROJECT_ID", + "value" : "$(PROJECT_ID)" + } + ], + "mainThreadCheckerEnabled" : false, + "targetForVariableExpansion" : { + "containerPath" : "container:ExampleApp.xcodeproj", + "identifier" : "844749F229B9E5B9005F520B", + "name" : "RelayIntegrationTests" + }, + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:ExampleApp.xcodeproj", + "identifier" : "844749F229B9E5B9005F520B", + "name" : "RelayIntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index b31c33fb0..5e1111b53 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -18,7 +18,7 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: importAccount.account, onSign: onSing) + Web3Inbox.configure(account: importAccount.account, config: [.pushEnabled: false], onSign: onSing, environment: .sandbox) edgesForExtendedLayout = [] navigationItem.title = "Web3Inbox SDK" diff --git a/Example/SmokeTests.xctestplan b/Example/SmokeTests.xctestplan index a5ce3e0df..34685f639 100644 --- a/Example/SmokeTests.xctestplan +++ b/Example/SmokeTests.xctestplan @@ -4,6 +4,7 @@ "id" : "EB662E8C-8DC0-4689-A2FF-B41B80A99F85", "name" : "Configuration 1", "options" : { + "mainThreadCheckerEnabled" : false, "targetForVariableExpansion" : { "containerPath" : "container:ExampleApp.xcodeproj", "identifier" : "A5E03DEC286464DB00888481", @@ -13,6 +14,7 @@ } ], "defaultOptions" : { + "codeCoverage" : false, "environmentVariableEntries" : [ { "key" : "RELAY_HOST", @@ -23,6 +25,7 @@ "value" : "$(PROJECT_ID)" } ], + "mainThreadCheckerEnabled" : false, "testTimeoutsEnabled" : true }, "testTargets" : [ @@ -39,13 +42,16 @@ "ChatTests\/testInvite()", "EIP1271VerifierTests", "EIP191VerifierTests", + "EIP55Tests", "ENSResolverTests", "PairingTests", "PushTests\/testDappDeletePushSubscription()", "PushTests\/testRequestPush()", "PushTests\/testWalletApprovesPushRequest()", + "PushTests\/testWalletCreatesSubscription()", "PushTests\/testWalletDeletePushSubscription()", "PushTests\/testWalletRejectsPushRequest()", + "PushTests\/testWalletUpdatesSubscription()", "RegistryTests", "RelayClientEndToEndTests", "SignClientTests\/testCaip25SatisfyAllRequiredAllOptionalNamespacesSuccessful()", diff --git a/Example/UITests/Regression/PushNotificationTests.swift b/Example/UITests/Regression/PushNotificationTests.swift deleted file mode 100644 index c37df7a66..000000000 --- a/Example/UITests/Regression/PushNotificationTests.swift +++ /dev/null @@ -1,21 +0,0 @@ - -import XCTest - -class PushNotificationTests: XCTestCase { - let wallet = XCUIApplication(bundleIdentifier: "com.walletconnect.example") - let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") - - func testPushNotification() { - wallet.launch() - - sleep(1) - - // Launch springboard - springboard.activate() - let text = "Is this working" - - let notification = springboard.otherElements["Notification"].descendants(matching: .any)["NotificationShortLookView"] - XCTAssertTrue(notification.waitForExistence(timeout: 5)) - notification.tap() - } -} diff --git a/Example/WalletApp/ApplicationLayer/AppDelegate.swift b/Example/WalletApp/ApplicationLayer/AppDelegate.swift index 25fa75d10..d294b73f6 100644 --- a/Example/WalletApp/ApplicationLayer/AppDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/AppDelegate.swift @@ -24,12 +24,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { - Task(priority: .high) { - // Use pasteboard for testing purposes - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - let pasteboard = UIPasteboard.general - pasteboard.string = token + + Task(priority: .high) { try await Push.wallet.register(deviceToken: deviceToken) } } diff --git a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index e06467da7..58064d376 100644 --- a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -1,6 +1,6 @@ import WalletConnectNetworking import Web3Wallet -import WalletConnectPush +import Web3Inbox struct ThirdPartyConfigurator: Configurator { @@ -16,9 +16,20 @@ struct ThirdPartyConfigurator: Configurator { ) Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) - Push.configure(environment: BuildConfiguration.shared.apnsEnvironment) + + let account = Account(blockchain: Blockchain("eip155:1")!, address: EthKeyStore.shared.address)! + + Web3Inbox.configure(account: account, config: [.chatEnabled: false, .settingsEnabled: false], onSign: Web3InboxSigner.onSing, environment: BuildConfiguration.shared.apnsEnvironment) } } +class Web3InboxSigner { + static func onSing(_ message: String) -> SigningResult { + let privateKey = EthKeyStore.shared.privateKeyRaw + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() + let signature = try! signer.sign(message: message, privateKey: privateKey, type: .eip191) + return .signed(signature) + } +} diff --git a/Example/WalletApp/ApplicationLayer/PushRegisterer.swift b/Example/WalletApp/ApplicationLayer/PushRegisterer.swift index 1f5f19162..91a7d63d5 100644 --- a/Example/WalletApp/ApplicationLayer/PushRegisterer.swift +++ b/Example/WalletApp/ApplicationLayer/PushRegisterer.swift @@ -8,33 +8,25 @@ class PushRegisterer { private var publishers = [AnyCancellable]() func getNotificationSettings() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - print("Notification settings: \(settings)") - guard settings.authorizationStatus == .authorized else { return } - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } + UNUserNotificationCenter.current().getNotificationSettings { settings in + print("Notification settings: \(settings)") + + guard settings.authorizationStatus == .authorized else { return } + + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } } func registerForPushNotifications() { UNUserNotificationCenter.current() - .requestAuthorization( - options: [.alert, .sound, .badge]) { [weak self] granted, _ in - print("Permission granted: \(granted)") + .requestAuthorization( + options: [.alert, .sound, .badge] + ) { granted, error in + print("Permission granted: \(granted)") guard granted else { return } - self?.getNotificationSettings() -#if targetEnvironment(simulator) - Networking.interactor.socketConnectionStatusPublisher - .first {$0 == .connected} - .sink{ status in - let deviceToken = InputConfig.simulatorIdentifier - assert(deviceToken != "SIMULATOR_IDENTIFIER", "Please set your Simulator identifier") - Task(priority: .high) { - try await Push.wallet.register(deviceToken: deviceToken) - } - }.store(in: &self!.publishers) -#endif + self.getNotificationSettings() } } } diff --git a/Example/WalletApp/Common/InputConfig.swift b/Example/WalletApp/Common/InputConfig.swift index 4cdd5ee73..83b9fac4e 100644 --- a/Example/WalletApp/Common/InputConfig.swift +++ b/Example/WalletApp/Common/InputConfig.swift @@ -4,12 +4,6 @@ struct InputConfig { static var projectId: String { return config(for: "PROJECT_ID")! } - -#if targetEnvironment(simulator) - static var simulatorIdentifier: String { - return config(for: "SIMULATOR_IDENTIFIER")! - } -#endif private static func config(for key: String) -> String? { return Bundle.main.object(forInfoDictionaryKey: key) as? String diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 1ace0f96a..ee968adb0 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -14,7 +14,7 @@ final class MainPresenter { var viewControllers: [UIViewController] { return [ router.walletViewController(), - router.notificationsViewController(), + router.web3InboxViewController() ] } @@ -34,7 +34,8 @@ extension MainPresenter { interactor.pushRequestPublisher .receive(on: DispatchQueue.main) .sink { [weak self] request in - self?.router.present(pushRequest: request) + +// self?.router.present(pushRequest: request) }.store(in: &disposeBag) interactor.sessionProposalPublisher diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index b8ea7765b..1bd145592 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -24,6 +24,11 @@ final class MainRouter { .wrapToNavigationController() } + func web3InboxViewController() -> UIViewController { + return Web3InboxModule.create(app: app) + .wrapToNavigationController() + } + func present(pushRequest: PushRequest) { PushRequestModule.create(app: app, pushRequest: pushRequest) .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 0416f515c..d195c90ae 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift @@ -2,14 +2,17 @@ import UIKit enum TabPage: CaseIterable { case wallet - case notifications +// case notifications + case web3Inbox var title: String { switch self { case .wallet: return "Apps" - case .notifications: - return "Notifications" +// case .notifications: +// return "Notifications" + case .web3Inbox: + return "w3i" } } @@ -17,7 +20,9 @@ enum TabPage: CaseIterable { switch self { case .wallet: return UIImage(systemName: "house.fill")! - case .notifications: +// case .notifications: +// return UIImage(systemName: "bell.fill")! + case .web3Inbox: return UIImage(systemName: "bell.fill")! } } @@ -27,6 +32,6 @@ enum TabPage: CaseIterable { } static var enabledTabs: [TabPage] { - return [.wallet, .notifications] + return [.wallet, .web3Inbox] } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift index 987c7920c..2afda2ff4 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift @@ -3,17 +3,10 @@ import WalletConnectPush final class PushRequestInteractor { func approve(pushRequest: PushRequest) async throws { - try await Push.wallet.approve(id: pushRequest.id, onSign: onSing(_:)) + try await Push.wallet.approve(id: pushRequest.id, onSign: Web3InboxSigner.onSing) } func reject(pushRequest: PushRequest) async throws { try await Push.wallet.reject(id: pushRequest.id) } - - func onSing(_ message: String) async -> SigningResult { - let privateKey = EthKeyStore.shared.privateKeyRaw - let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() - let signature = try! signer.sign(message: message, privateKey: privateKey, type: .eip191) - return .signed(signature) - } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 968687208..910a5288e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -7,9 +7,6 @@ final class SessionRequestPresenter: ObservableObject { private let interactor: SessionRequestInteractor private let router: SessionRequestRouter - @Published var showError = false - @Published var errorMessage = "Error" - let sessionRequest: Request let verified: Bool? @@ -17,6 +14,9 @@ final class SessionRequestPresenter: ObservableObject { return String(describing: sessionRequest.params.value) } + @Published var showError = false + @Published var errorMessage = "Error" + private var disposeBag = Set() init( diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index b1c406c3f..930b3472e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -4,12 +4,19 @@ import Combine import Web3Wallet final class WalletPresenter: ObservableObject { + enum Errors: Error { + case invalidUri(uri: String) + } + private let interactor: WalletInteractor private let router: WalletRouter + private let uri: String? + @Published var sessions = [Session]() - private let uri: String? + @Published var showError = false + @Published var errorMessage = "Error" private var disposeBag = Set() @@ -33,9 +40,11 @@ final class WalletPresenter: ObservableObject { func onPasteUri() { router.presentPaste { [weak self] uri in guard let uri = WalletConnectURI(string: uri) else { + self?.errorMessage = Errors.invalidUri(uri: uri).localizedDescription + self?.showError.toggle() return } - print(uri) + print("URI: \(uri)") self?.pair(uri: uri) } onError: { [weak self] error in @@ -45,10 +54,15 @@ final class WalletPresenter: ObservableObject { } func onScanUri() { - router.presentScan { [unowned self] value in - guard let uri = WalletConnectURI(string: value) else { return } - self.pair(uri: uri) - self.router.dismiss() + router.presentScan { [weak self] uri in + guard let uri = WalletConnectURI(string: uri) else { + self?.errorMessage = Errors.invalidUri(uri: uri).localizedDescription + self?.showError.toggle() + return + } + print("URI: \(uri)") + self?.pair(uri: uri) + self?.router.dismiss() } onError: { error in print(error.localizedDescription) self.router.dismiss() @@ -84,9 +98,17 @@ extension WalletPresenter { pairFromDapp() } + private func pair(uri: WalletConnectURI) { Task(priority: .high) { [unowned self] in - try await self.interactor.pair(uri: uri) + do { + try await self.interactor.pair(uri: uri) + } catch { + Task.detached { @MainActor in + self.errorMessage = error.localizedDescription + self.showError.toggle() + } + } } } @@ -116,3 +138,12 @@ extension WalletPresenter: SceneViewModel { return .always } } + +// MARK: - LocalizedError +extension WalletPresenter.Errors: LocalizedError { + var errorDescription: String? { + switch self { + case .invalidUri(let uri): return "URI invalid format\n\(uri)" + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift index 2c889076a..c4dddd391 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -70,6 +70,9 @@ struct WalletView: View { } .padding(.vertical, 20) } + .alert(presenter.errorMessage, isPresented: $presenter.showError) { + Button("OK", role: .cancel) {} + } } private func connectionView(session: Session) -> some View { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift new file mode 100644 index 000000000..ad0633441 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxModule.swift @@ -0,0 +1,13 @@ +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/Web3InboxRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxRouter.swift new file mode 100644 index 000000000..3631c35be --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxRouter.swift @@ -0,0 +1,12 @@ +import UIKit + +final class Web3InboxRouter { + + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift new file mode 100644 index 000000000..e4043cca5 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift @@ -0,0 +1,27 @@ +import UIKit +import WebKit +import Web3Inbox + +final class Web3InboxViewController: UIViewController { + + + 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() + } +} + + + diff --git a/Example/WalletApp/PresentationLayer/Wallet/Welcome/WelcomePresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Welcome/WelcomePresenter.swift index 136dc41da..2b60f80ab 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Welcome/WelcomePresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Welcome/WelcomePresenter.swift @@ -16,6 +16,7 @@ final class WelcomePresenter: ObservableObject { } func onGetStarted() { + let pasteboard = UIPasteboard.general let clientId = try? Networking.interactor.getClientId() pasteboard.string = clientId diff --git a/Makefile b/Makefile index 04ea64890..a816db3f0 100755 --- a/Makefile +++ b/Makefile @@ -16,38 +16,75 @@ ifeq "${EXISTS_FASTLANE}" "" endif @echo "All dependencies was installed" +test_setup: + defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES + rm -rf test_results + mkdir test_results + +build_all: + rm -rf test_results + set -o pipefail && xcodebuild -scheme "WalletConnect-Package" -destination "platform=iOS Simulator,name=iPhone 14" -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' build-for-testing | xcpretty + set -o pipefail && xcodebuild -project "Example/ExampleApp.xcodeproj" -scheme "BuildAll" -destination "platform=iOS Simulator,name=iPhone 14" -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' build-for-testing | xcpretty + build_dapp: fastlane build scheme:DApp build_wallet: fastlane build scheme:WalletApp +echo_ui_tests: + echo "EchoUITests disabled" + ui_tests: echo "UI Tests disabled" -unit_tests: - fastlane tests scheme:WalletConnect - -integration_tests: - fastlane tests scheme:IntegrationTests testplan:IntegrationTests relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) - -relay_tests: - fastlane tests scheme:RelayIntegrationTests relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) - -smoke_tests: - xcodebuild \ - -project Example/ExampleApp.xcodeproj \ - -scheme IntegrationTests \ - -testPlan SmokeTests \ - -clonedSourcePackagesDirPath SourcePackagesCache \ - -destination 'platform=iOS Simulator,name=iPhone 14' \ - -derivedDataPath DerivedDataCache \ - RELAY_HOST=$(RELAY_HOST) \ - PROJECT_ID=$(PROJECT_ID) \ - test - -resolve_packages: - fastlane resolve scheme:WalletApp +unitxctestrun = $(shell find . -name '*WalletConnect-Package*.xctestrun') + +unit_tests: test_setup +ifneq ($(unitxctestrun),) + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/UnitTests.xcresult' -xctestrun '$(unitxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +else + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme WalletConnect-Package -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/UnitTests.xcresult' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +endif + +integrationxctestrun = $(shell find . -name '*_IntegrationTests*.xctestrun') + +integration_tests: test_setup +ifneq ($(integrationxctestrun),) +# override ENV variables + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(integrationxctestrun) + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(integrationxctestrun) +# test-without-building + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/IntegrationTests.xcresult' -xctestrun '$(integrationxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +else + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme IntegrationTests -testPlan IntegrationTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/IntegrationTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +endif + +relayxctestrun = $(shell find . -name '*_RelayIntegrationTests*.xctestrun') + +relay_tests: test_setup +ifneq ($(relayxctestrun),) +# override ENV variables + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(relayxctestrun) + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(relayxctestrun) +# test-without-building + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/RelayIntegrationTests.xcresult' -xctestrun '$(relayxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +else + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme RelayIntegrationTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/RelayIntegrationTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +endif + +smokexctestrun = $(shell find . -name '*_SmokeTests*.xctestrun') + +smoke_tests: test_setup +ifneq ($(smokexctestrun),) +# override ENV variables + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(smokexctestrun) + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(smokexctestrun) +# test-without-building + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/SmokeTests.xcresult' -xctestrun '$(smokexctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +else + set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme IntegrationTests -testPlan SmokeTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/SmokeTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit +endif release_wallet: fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) --env WalletApp diff --git a/Package.swift b/Package.swift index 1800221e1..f475f4684 100644 --- a/Package.swift +++ b/Package.swift @@ -64,7 +64,7 @@ let package = Package( path: "Sources/Web3Wallet"), .target( name: "WalletConnectPush", - dependencies: ["WalletConnectPairing", "WalletConnectEcho", "WalletConnectNetworking", "WalletConnectIdentity"], + dependencies: ["WalletConnectPairing", "WalletConnectEcho", "WalletConnectNetworking", "WalletConnectIdentity", "WalletConnectSigner"], path: "Sources/WalletConnectPush"), .target( name: "WalletConnectEcho", @@ -84,7 +84,7 @@ let package = Package( dependencies: ["WalletConnectNetworking"]), .target( name: "Web3Inbox", - dependencies: ["WalletConnectChat"]), + dependencies: ["WalletConnectChat", "WalletConnectPush"]), .target( name: "WalletConnectSigner", dependencies: ["WalletConnectNetworking"]), diff --git a/Sources/WalletConnectJWT/JWTEncoder.swift b/Sources/WalletConnectJWT/JWTEncoder.swift index f9a2d5425..fd2ec386f 100644 --- a/Sources/WalletConnectJWT/JWTEncoder.swift +++ b/Sources/WalletConnectJWT/JWTEncoder.swift @@ -13,17 +13,8 @@ struct JWTEncoder { } public static func base64urlDecodedData(string: String) throws -> Data { - var base64 = string - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - - if base64.count % 4 != 0 { - base64.append(String(repeating: "=", count: 4 - base64.count % 4)) - } - - guard let result = Data(base64Encoded: base64) + guard let result = Data(base64url: string) else { throw JWTError.notBase64String } - return result } } diff --git a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift index 01f0ed2a2..f5e0f906e 100644 --- a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift +++ b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift @@ -18,6 +18,9 @@ extension Curve25519.KeyAgreement.PrivateKey: Equatable { // MARK: - Public Key public struct AgreementPublicKey: GenericPasswordConvertible, Equatable { + enum Errors: Error { + case invalidBase64urlString + } fileprivate let key: Curve25519.KeyAgreement.PublicKey @@ -34,6 +37,11 @@ public struct AgreementPublicKey: GenericPasswordConvertible, Equatable { try self.init(rawRepresentation: data) } + public init(base64url: String) throws { + guard let raw = Data(base64url: base64url) else { throw Errors.invalidBase64urlString } + try self.init(rawRepresentation: raw) + } + public var rawRepresentation: Data { key.rawRepresentation } diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index ede99be10..1f8a7cebf 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -134,7 +134,7 @@ public class NetworkingInteractor: NetworkInteracting { if let (deserializedJsonRpcRequest, derivedTopic, decryptedPayload): (RPCRequest, String?, Data) = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { handleRequest(topic: topic, request: deserializedJsonRpcRequest, decryptedPayload: decryptedPayload, publishedAt: publishedAt, derivedTopic: derivedTopic) } else if let (response, derivedTopic, _): (RPCResponse, String?, Data) = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleResponse(response: response, publishedAt: publishedAt, derivedTopic: derivedTopic) + handleResponse(topic: topic, response: response, publishedAt: publishedAt, derivedTopic: derivedTopic) } else { logger.debug("Networking Interactor - Received unknown object type from networking relay") } @@ -149,11 +149,11 @@ public class NetworkingInteractor: NetworkInteracting { } } - private func handleResponse(response: RPCResponse, publishedAt: Date, derivedTopic: String?) { + private func handleResponse(topic: String, response: RPCResponse, publishedAt: Date, derivedTopic: String?) { do { try rpcHistory.resolve(response) let record = rpcHistory.get(recordId: response.id!)! - responsePublisherSubject.send((record.topic, record.request, response, publishedAt, derivedTopic)) + responsePublisherSubject.send((topic, record.request, response, publishedAt, derivedTopic)) } catch { logger.debug("Handle json rpc response error: \(error)") } diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift index 0ec9e485b..cfbd6c930 100644 --- a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift +++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift @@ -2,7 +2,7 @@ import Foundation actor WalletPairService { enum Errors: Error { - case pairingAlreadyExist + case pairingAlreadyExist(topic: String) } let networkingInteractor: NetworkInteracting @@ -19,7 +19,7 @@ actor WalletPairService { func pair(_ uri: WalletConnectURI) async throws { guard !hasPairing(for: uri.topic) else { - throw Errors.pairingAlreadyExist + throw Errors.pairingAlreadyExist(topic: uri.topic) } var pairing = WCPairing(uri: uri) let symKey = try SymmetricKey(hex: uri.symKey) @@ -33,3 +33,12 @@ actor WalletPairService { return pairingStorage.hasPairing(forTopic: topic) } } + +// MARK: - LocalizedError +extension WalletPairService.Errors: LocalizedError { + var errorDescription: String? { + switch self { + case .pairingAlreadyExist(let topic): return "Pairing with topic (\(topic)) already exist" + } + } +} diff --git a/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift index 603b81bbb..e131f075b 100644 --- a/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift +++ b/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift @@ -35,7 +35,7 @@ class ProposalResponseSubscriber { private func subscribeForProposalResponse() { let protocolMethod = PushRequestProtocolMethod() networkingInteractor.responseSubscription(on: protocolMethod) - .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in logger.debug("Received Push Proposal response") Task(priority: .userInitiated) { do { @@ -49,15 +49,16 @@ class ProposalResponseSubscriber { }.store(in: &publishers) } - private func handleResponse(payload: ResponseSubscriptionPayload) async throws -> (PushSubscription, String) { + private func handleResponse(payload: ResponseSubscriptionPayload) async throws -> (PushSubscription, String) { let jwt = payload.response.jwtString - _ = try AcceptSubscriptionJWTPayload.decodeAndVerify(from: payload.response) + let (_, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: payload.response) logger.debug("subscriptionAuth JWT validated") guard let subscriptionTopic = payload.derivedTopic else { throw Errors.subscriptionTopicNotDerived } + let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) - let pushSubscription = PushSubscription(topic: subscriptionTopic, account: payload.request.account, relay: relay, metadata: metadata) + let pushSubscription = PushSubscription(topic: subscriptionTopic, account: payload.request.account, relay: relay, metadata: metadata, scope: [:], expiry: expiry) logger.debug("Subscribing to Push Subscription topic: \(subscriptionTopic)") subscriptionsStore.set(pushSubscription, forKey: subscriptionTopic) try await networkingInteractor.subscribe(topic: subscriptionTopic) diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift new file mode 100644 index 000000000..402d8084c --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift @@ -0,0 +1,51 @@ + +import Foundation + +class NotifyUpdateRequester { + enum Errors: Error { + case noSubscriptionForGivenTopic + } + + private let keyserverURL: URL + private let identityClient: IdentityClient + private let networkingInteractor: NetworkInteracting + private let logger: ConsoleLogging + private let subscriptionsStore: CodableStore + + init(keyserverURL: URL, + identityClient: IdentityClient, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + subscriptionsStore: CodableStore + ) { + self.keyserverURL = keyserverURL + self.identityClient = identityClient + self.networkingInteractor = networkingInteractor + self.logger = logger + self.subscriptionsStore = subscriptionsStore + } + + func update(topic: String, scope: Set) async throws { + + logger.debug("NotifyUpdateRequester: updating subscription for topic: \(topic)") + guard let subscription = try subscriptionsStore.get(key: topic) else { throw Errors.noSubscriptionForGivenTopic } + + let request = try createJWTRequest(subscriptionAccount: subscription.account, dappUrl: subscription.metadata.url, scope: scope) + + let protocolMethod = NotifyUpdateProtocolMethod() + + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + } + + private func createJWTRequest(subscriptionAccount: Account, dappUrl: String, scope: Set) throws -> RPCRequest { + let protocolMethod = NotifyUpdateProtocolMethod().method + let scopeClaim = scope.map {$0.rawValue}.joined(separator: " ") + let jwtPayload = SubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scopeClaim) + let wrapper = try identityClient.signAndCreateWrapper( + payload: jwtPayload, + account: subscriptionAccount + ) + print(wrapper.subscriptionAuth) + return RPCRequest(method: protocolMethod, params: wrapper) + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift new file mode 100644 index 000000000..5847d1185 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift @@ -0,0 +1,72 @@ + +import Foundation +import Combine + +class NotifyUpdateResponseSubscriber { + enum Errors: Error { + case subscriptionDoesNotExist + } + + private let networkingInteractor: NetworkInteracting + private var publishers = [AnyCancellable]() + private let logger: ConsoleLogging + private let subscriptionsStore: CodableStore + private let subscriptionScopeProvider: SubscriptionScopeProvider + private var subscriptionPublisherSubject = PassthroughSubject, Never>() + var updateSubscriptionPublisher: AnyPublisher, Never> { + return subscriptionPublisherSubject.eraseToAnyPublisher() + } + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + subscriptionScopeProvider: SubscriptionScopeProvider, + subscriptionsStore: CodableStore + ) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.subscriptionsStore = subscriptionsStore + self.subscriptionScopeProvider = subscriptionScopeProvider + subscribeForUpdateResponse() + } + + private func subscribeForUpdateResponse() { + let protocolMethod = NotifyUpdateProtocolMethod() + networkingInteractor.responseSubscription(on: protocolMethod) + .sink {[unowned self] (payload: ResponseSubscriptionPayload) in + Task(priority: .high) { + logger.debug("Received Push Update response") + + let subscriptionTopic = payload.topic + + let (_, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: payload.request) + let scope = try await buildScope(selected: claims.scp, dappUrl: claims.aud) + + guard let oldSubscription = try? subscriptionsStore.get(key: subscriptionTopic) else { + logger.debug("NotifyUpdateResponseSubscriber Subscription does not exist") + subscriptionPublisherSubject.send(.failure(Errors.subscriptionDoesNotExist)) + return + } + let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) + + let updatedSubscription = PushSubscription(topic: subscriptionTopic, account: oldSubscription.account, relay: oldSubscription.relay, metadata: oldSubscription.metadata, scope: scope, expiry: expiry) + + subscriptionsStore.set(updatedSubscription, forKey: subscriptionTopic) + + subscriptionPublisherSubject.send(.success(updatedSubscription)) + + logger.debug("Updated Subscription") + } + }.store(in: &publishers) + } + + private func buildScope(selected: String, dappUrl: String) async throws -> [NotificationScope: ScopeValue] { + let selectedScope = selected + .components(separatedBy: " ") + .compactMap { NotificationScope(rawValue: $0) } + + let availableScope = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) + return availableScope.reduce(into: [:]) { $0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name)) } + } + + // TODO: handle error response +} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushMessageSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushMessage/PushMessageSubscriber.swift similarity index 100% rename from Sources/WalletConnectPush/Client/Wallet/PushMessageSubscriber.swift rename to Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushMessage/PushMessageSubscriber.swift diff --git a/Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushRequest/PushRequestResponder.swift similarity index 87% rename from Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift rename to Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushRequest/PushRequestResponder.swift index ae7b83357..cfebe88a3 100644 --- a/Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift +++ b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushRequest/PushRequestResponder.swift @@ -1,5 +1,6 @@ import WalletConnectNetworking import WalletConnectIdentity +import Combine import Foundation class PushRequestResponder { @@ -17,6 +18,10 @@ class PushRequestResponder { // Keychain shared with UNNotificationServiceExtension in order to decrypt PNs private let groupKeychainStorage: KeychainStorageProtocol + private var subscriptionPublisherSubject = PassthroughSubject, Never>() + var subscriptionPublisher: AnyPublisher, Never> { + return subscriptionPublisherSubject.eraseToAnyPublisher() + } init(keyserverURL: URL, networkingInteractor: NetworkInteracting, @@ -65,13 +70,16 @@ class PushRequestResponder { let response = try createJWTResponse(requestId: requestId, subscriptionAccount: requestParams.account, dappUrl: requestParams.metadata.url) - let pushSubscription = PushSubscription(topic: pushTopic, account: requestParams.account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: requestParams.metadata) + // will be changed in stage 2 refactor, this method will depricate + let expiry = Date() + let pushSubscription = PushSubscription(topic: pushTopic, account: requestParams.account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: requestParams.metadata, scope: [:], expiry: expiry) subscriptionsStore.set(pushSubscription, forKey: pushTopic) try await networkingInteractor.respond(topic: responseTopic, response: response, protocolMethod: PushRequestProtocolMethod(), envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) kms.deletePrivateKey(for: keys.publicKey.hexRepresentation) + subscriptionPublisherSubject.send(.success(pushSubscription)) } func respondError(requestId: RPCID) async throws { @@ -83,7 +91,7 @@ class PushRequestResponder { } private func createJWTResponse(requestId: RPCID, subscriptionAccount: Account, dappUrl: String) throws -> RPCResponse { - let jwtPayload = AcceptSubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl) + let jwtPayload = SubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: "v1") let wrapper = try identityClient.signAndCreateWrapper( payload: jwtPayload, account: subscriptionAccount diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift new file mode 100644 index 000000000..49217c554 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift @@ -0,0 +1,98 @@ + +import Foundation + +class PushSubscribeRequester { + + enum Errors: Error { + case didDocDoesNotContainKeyAgreement + case noVerificationMethodForKey + case unsupportedCurve + } + + private let keyserverURL: URL + private let identityClient: IdentityClient + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementService + private let logger: ConsoleLogging + private let webDidResolver: WebDidResolver + private let dappsMetadataStore: CodableStore + + init(keyserverURL: URL, + networkingInteractor: NetworkInteracting, + identityClient: IdentityClient, + logger: ConsoleLogging, + kms: KeyManagementService, + webDidResolver: WebDidResolver = WebDidResolver(), + dappsMetadataStore: CodableStore + ) { + self.keyserverURL = keyserverURL + self.identityClient = identityClient + self.networkingInteractor = networkingInteractor + self.logger = logger + self.kms = kms + self.webDidResolver = webDidResolver + self.dappsMetadataStore = dappsMetadataStore + } + + func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws { + + let dappUrl = metadata.url + + logger.debug("Subscribing for Push") + + let peerPublicKey = try await resolvePublicKey(dappUrl: metadata.url) + 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 await identityClient.register(account: account, onSign: onSign) + + try kms.setAgreementSecret(keysY, topic: responseTopic) + + logger.debug("setting symm key for response topic \(responseTopic)") + + let request = try createJWTRequest(subscriptionAccount: account, dappUrl: dappUrl) + + let protocolMethod = PushSubscribeProtocolMethod() + + logger.debug("PushSubscribeRequester: subscribing to response topic: \(responseTopic)") + + try await networkingInteractor.subscribe(topic: responseTopic) + + try await networkingInteractor.request(request, topic: subscribeTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: keysY.publicKey.rawRepresentation)) + } + + private func resolvePublicKey(dappUrl: String) async throws -> AgreementPublicKey { + logger.debug("PushSubscribeRequester: Resolving DIDDoc for: \(dappUrl)") + let didDoc = try await webDidResolver.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 func generateAgreementKeys(peerPublicKey: AgreementPublicKey) throws -> AgreementKeys { + let selfPubKey = try kms.createX25519KeyPair() + + let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPublicKey.hexRepresentation) + return keys + } + + private func createJWTRequest(subscriptionAccount: Account, dappUrl: String) throws -> RPCRequest { + let protocolMethod = PushSubscribeProtocolMethod().method + let jwtPayload = SubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: "") + let wrapper = try identityClient.signAndCreateWrapper( + payload: jwtPayload, + account: subscriptionAccount + ) + return RPCRequest(method: protocolMethod, params: wrapper) + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift new file mode 100644 index 000000000..63e8e0fa3 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift @@ -0,0 +1,100 @@ + +import Foundation +import Combine + +class PushSubscribeResponseSubscriber { + enum Errors: Error { + case couldNotCreateSubscription + } + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private var publishers = [AnyCancellable]() + private let logger: ConsoleLogging + private let subscriptionsStore: CodableStore + private let groupKeychainStorage: KeychainStorageProtocol + private let dappsMetadataStore: CodableStore + private let subscriptionScopeProvider: SubscriptionScopeProvider + private var subscriptionPublisherSubject = PassthroughSubject, Never>() + var subscriptionPublisher: AnyPublisher, Never> { + return subscriptionPublisherSubject.eraseToAnyPublisher() + } + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + groupKeychainStorage: KeychainStorageProtocol, + subscriptionsStore: CodableStore, + dappsMetadataStore: CodableStore, + subscriptionScopeProvider: SubscriptionScopeProvider + ) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.groupKeychainStorage = groupKeychainStorage + self.subscriptionsStore = subscriptionsStore + self.dappsMetadataStore = dappsMetadataStore + self.subscriptionScopeProvider = subscriptionScopeProvider + subscribeForSubscriptionResponse() + } + + private func subscribeForSubscriptionResponse() { + let protocolMethod = PushSubscribeProtocolMethod() + networkingInteractor.responseSubscription(on: protocolMethod) + .sink {[unowned self] (payload: ResponseSubscriptionPayload) in + Task(priority: .high) { + logger.debug("Received Push Subscribe response") + + guard let responseKeys = kms.getAgreementSecret(for: payload.topic) else { + logger.debug("PushSubscribeResponseSubscriber: no symmetric key for topic \(payload.topic)") + subscriptionPublisherSubject.send(.failure(Errors.couldNotCreateSubscription)) + return + } + + // get keypair Y + let pubKeyY = responseKeys.publicKey + let peerPubKeyZ = payload.response.publicKey + + var account: Account! + var metadata: AppMetadata! + var pushSubscriptionTopic: String! + var availableScope: Set! + let (_, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: payload.request) + do { + // generate symm key P + let agreementKeysP = try kms.performKeyAgreement(selfPublicKey: pubKeyY, peerPublicKey: peerPubKeyZ) + pushSubscriptionTopic = agreementKeysP.derivedTopic() + try kms.setAgreementSecret(agreementKeysP, topic: pushSubscriptionTopic) + try groupKeychainStorage.add(agreementKeysP, forKey: pushSubscriptionTopic) + account = try Account(DIDPKHString: claims.sub) + metadata = try dappsMetadataStore.get(key: payload.topic) + availableScope = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: metadata!.url) + logger.debug("PushSubscribeResponseSubscriber: subscribing push subscription topic: \(pushSubscriptionTopic!)") + try await networkingInteractor.subscribe(topic: pushSubscriptionTopic) + } catch { + logger.debug("PushSubscribeResponseSubscriber: error: \(error)") + subscriptionPublisherSubject.send(.failure(Errors.couldNotCreateSubscription)) + return + } + + guard let metadata = metadata else { + logger.debug("PushSubscribeResponseSubscriber: no metadata for topic: \(pushSubscriptionTopic)") + return + } + dappsMetadataStore.delete(forKey: payload.topic) + let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) + let scope: [NotificationScope: ScopeValue] = availableScope.reduce(into: [:]) { $0[$1.name] = ScopeValue(description: $1.description, enabled: true) } + let pushSubscription = PushSubscription(topic: pushSubscriptionTopic, account: account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: metadata, scope: scope, expiry: expiry) + + subscriptionsStore.set(pushSubscription, forKey: pushSubscriptionTopic) + + logger.debug("PushSubscribeResponseSubscriber: unsubscribing response topic: \(payload.topic)") + networkingInteractor.unsubscribe(topic: payload.topic) + subscriptionPublisherSubject.send(.success(pushSubscription)) + } + }.store(in: &publishers) + } + + // TODO: handle error response + +} diff --git a/Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift b/Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift new file mode 100644 index 000000000..a2140addd --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift @@ -0,0 +1,22 @@ + +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-push-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/WalletConnectPush/Client/Wallet/WalletPushClient.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift index c7848a79e..2740cb252 100644 --- a/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift +++ b/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift @@ -9,6 +9,13 @@ public class WalletPushClient { private var publishers = Set() + /// publishes new subscriptions + public var subscriptionPublisher: AnyPublisher, Never> { + return subscriptionPublisherSubject.eraseToAnyPublisher() + } + + public var subscriptionPublisherSubject = PassthroughSubject, Never>() + public var subscriptionsPublisher: AnyPublisher<[PushSubscription], Never> { return pushSubscriptionsObserver.subscriptionsPublisher } @@ -33,8 +40,13 @@ public class WalletPushClient { deleteSubscriptionPublisherSubject.eraseToAnyPublisher() } + public var updateSubscriptionPublisher: AnyPublisher, Never> { + return notifyUpdateResponseSubscriber.updateSubscriptionPublisher + } + private let deletePushSubscriptionService: DeletePushSubscriptionService private let deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber + private let pushSubscribeRequester: PushSubscribeRequester public let logger: ConsoleLogging @@ -45,6 +57,9 @@ public class WalletPushClient { private let subscriptionsProvider: SubscriptionsProvider private let pushMessagesDatabase: PushMessagesDatabase private let resubscribeService: PushResubscribeService + private let pushSubscribeResponseSubscriber: PushSubscribeResponseSubscriber + private let notifyUpdateRequester: NotifyUpdateRequester + private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber init(logger: ConsoleLogging, kms: KeyManagementServiceProtocol, @@ -57,7 +72,12 @@ public class WalletPushClient { deletePushSubscriptionService: DeletePushSubscriptionService, deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber, resubscribeService: PushResubscribeService, - pushSubscriptionsObserver: PushSubscriptionsObserver) { + pushSubscriptionsObserver: PushSubscriptionsObserver, + pushSubscribeRequester: PushSubscribeRequester, + pushSubscribeResponseSubscriber: PushSubscribeResponseSubscriber, + notifyUpdateRequester: NotifyUpdateRequester, + notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber + ) { self.logger = logger self.pairingRegisterer = pairingRegisterer self.proposeResponder = proposeResponder @@ -69,9 +89,17 @@ public class WalletPushClient { self.deletePushSubscriptionSubscriber = deletePushSubscriptionSubscriber self.resubscribeService = resubscribeService self.pushSubscriptionsObserver = pushSubscriptionsObserver + self.pushSubscribeRequester = pushSubscribeRequester + self.pushSubscribeResponseSubscriber = pushSubscribeResponseSubscriber + self.notifyUpdateRequester = notifyUpdateRequester + self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber setupSubscriptions() } + public func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws { + try await pushSubscribeRequester.subscribe(metadata: metadata, account: account, onSign: onSign) + } + public func approve(id: RPCID, onSign: @escaping SigningCallback) async throws { try await proposeResponder.respond(requestId: id, onSign: onSign) } @@ -80,6 +108,10 @@ public class WalletPushClient { try await proposeResponder.respondError(requestId: id) } + public func update(topic: String, scope: Set) async throws { + try await notifyUpdateRequester.update(topic: topic, scope: scope) + } + public func getActiveSubscriptions() -> [PushSubscription] { subscriptionsProvider.getActiveSubscriptions() } @@ -114,9 +146,18 @@ private extension WalletPushClient { pushMessageSubscriber.onPushMessage = { [unowned self] pushMessageRecord in pushMessagePublisherSubject.send(pushMessageRecord) } + deletePushSubscriptionSubscriber.onDelete = {[unowned self] topic in deleteSubscriptionPublisherSubject.send(topic) } + + pushSubscribeResponseSubscriber.subscriptionPublisher.sink { [unowned self] result in + subscriptionPublisherSubject.send(result) + }.store(in: &publishers) + + proposeResponder.subscriptionPublisher.sink { [unowned self] result in + subscriptionPublisherSubject.send(result) + }.store(in: &publishers) } } diff --git a/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift index ee88eb8de..adec22410 100644 --- a/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift +++ b/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift @@ -6,7 +6,7 @@ import WalletConnectIdentity public struct WalletPushClientFactory { public static func create(networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, echoClient: EchoClient) -> WalletPushClient { - let logger = ConsoleLogger(loggingLevel: .debug) + let logger = ConsoleLogger(suffix: "🔔",loggingLevel: .debug) let keyValueStorage = UserDefaults.standard let keyserverURL = URL(string: "https://keys.walletconnect.com")! let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") @@ -52,6 +52,19 @@ public struct WalletPushClientFactory { let deletePushSubscriptionSubscriber = DeletePushSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushSubscriptionStore: subscriptionStore) let resubscribeService = PushResubscribeService(networkInteractor: networkInteractor, subscriptionsStorage: subscriptionStore) let pushSubscriptionsObserver = PushSubscriptionsObserver(store: subscriptionStore) + + + let dappsMetadataStore = CodableStore(defaults: keyValueStorage, identifier: PushStorageIdntifiers.dappsMetadataStore) + + let pushSubscribeRequester = PushSubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, dappsMetadataStore: dappsMetadataStore) + + let subscriptionScopeProvider = SubscriptionScopeProvider() + let pushSubscribeResponseSubscriber = PushSubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, subscriptionsStore: subscriptionStore, dappsMetadataStore: dappsMetadataStore, subscriptionScopeProvider: subscriptionScopeProvider) + + let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, identityClient: identityClient, networkingInteractor: networkInteractor, logger: logger, subscriptionsStore: subscriptionStore) + + let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, subscriptionScopeProvider: subscriptionScopeProvider, subscriptionsStore: subscriptionStore) + return WalletPushClient( logger: logger, kms: kms, @@ -64,7 +77,11 @@ public struct WalletPushClientFactory { deletePushSubscriptionService: deletePushSubscriptionService, deletePushSubscriptionSubscriber: deletePushSubscriptionSubscriber, resubscribeService: resubscribeService, - pushSubscriptionsObserver: pushSubscriptionsObserver + pushSubscriptionsObserver: pushSubscriptionsObserver, + pushSubscribeRequester: pushSubscribeRequester, + pushSubscribeResponseSubscriber: pushSubscribeResponseSubscriber, + notifyUpdateRequester: notifyUpdateRequester, + notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber ) } } diff --git a/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift b/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift new file mode 100644 index 000000000..f7e6617ad --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift @@ -0,0 +1,15 @@ + +import Foundation + +class WebDidResolver { + + enum Errors: Error { + case invalidUrl + } + + 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/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift new file mode 100644 index 000000000..198ec859f --- /dev/null +++ b/Sources/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift @@ -0,0 +1,11 @@ + +import Foundation + +struct NotifyUpdateProtocolMethod: ProtocolMethod { + let method: String = "wc_pushUpdate" + + let requestConfig: RelayConfig = RelayConfig(tag: 4008, prompt: true, ttl: 86400) + + let responseConfig: RelayConfig = RelayConfig(tag: 4009, prompt: true, ttl: 86400) +} + diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift index 287b2a3bb..808817290 100644 --- a/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift +++ b/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift @@ -4,7 +4,7 @@ import WalletConnectPairing struct PushMessageProtocolMethod: ProtocolMethod { let method: String = "wc_pushMessage" - let requestConfig: RelayConfig = RelayConfig(tag: 4002, prompt: true, ttl: 86400) + let requestConfig: RelayConfig = RelayConfig(tag: 4002, prompt: true, ttl: 2592000) - let responseConfig: RelayConfig = RelayConfig(tag: 4003, prompt: true, ttl: 86400) + let responseConfig: RelayConfig = RelayConfig(tag: 4003, prompt: true, ttl: 2592000) } diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift new file mode 100644 index 000000000..a101dc0e5 --- /dev/null +++ b/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift @@ -0,0 +1,10 @@ + +import Foundation + +struct PushSubscribeProtocolMethod: ProtocolMethod { + let method: String = "wc_pushSubscribe" + + let requestConfig: RelayConfig = RelayConfig(tag: 4006, prompt: true, ttl: 86400) + + let responseConfig: RelayConfig = RelayConfig(tag: 4007, prompt: true, ttl: 86400) +} diff --git a/Sources/WalletConnectPush/PushStorageIdntifiers.swift b/Sources/WalletConnectPush/PushStorageIdntifiers.swift index 5dd0f0a53..57ac3611c 100644 --- a/Sources/WalletConnectPush/PushStorageIdntifiers.swift +++ b/Sources/WalletConnectPush/PushStorageIdntifiers.swift @@ -3,4 +3,5 @@ import Foundation enum PushStorageIdntifiers { static let pushSubscription = "com.walletconnect.sdk.pushSbscription" static let pushMessagesRecords = "com.walletconnect.sdk.pushMessagesRecords" + static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore" } diff --git a/Sources/WalletConnectPush/RPCRequests/SubscribeResponseParams.swift b/Sources/WalletConnectPush/RPCRequests/SubscribeResponseParams.swift new file mode 100644 index 000000000..51e75148f --- /dev/null +++ b/Sources/WalletConnectPush/RPCRequests/SubscribeResponseParams.swift @@ -0,0 +1,6 @@ + +import Foundation + +struct SubscribeResponseParams: Codable { + let publicKey: String +} diff --git a/Sources/WalletConnectPush/RPCRequests/AcceptSubscriptionJWTPayload.swift b/Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift similarity index 83% rename from Sources/WalletConnectPush/RPCRequests/AcceptSubscriptionJWTPayload.swift rename to Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift index 5594aec83..d45ac558b 100644 --- a/Sources/WalletConnectPush/RPCRequests/AcceptSubscriptionJWTPayload.swift +++ b/Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift @@ -1,6 +1,6 @@ import Foundation -struct AcceptSubscriptionJWTPayload: JWTClaimsCodable { +struct SubscriptionJWTPayload: JWTClaimsCodable { struct Claims: JWTClaims { /// timestamp when jwt was issued @@ -17,6 +17,8 @@ struct AcceptSubscriptionJWTPayload: JWTClaimsCodable { let sub: String /// description of action intent. Must be equal to "push_subscription" let act: String + + let scp: String } struct Wrapper: JWTWrapper { @@ -34,28 +36,33 @@ struct AcceptSubscriptionJWTPayload: JWTClaimsCodable { let keyserver: URL let subscriptionAccount: Account let dappUrl: String + let scope: String - init(keyserver: URL, subscriptionAccount: Account, dappUrl: String) { + init(keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { self.keyserver = keyserver self.subscriptionAccount = subscriptionAccount self.dappUrl = dappUrl + self.scope = scope } init(claims: Claims) throws { self.keyserver = try claims.ksu.asURL() self.subscriptionAccount = try Account(DIDPKHString: claims.sub) self.dappUrl = claims.aud + self.scope = claims.scp } func encode(iss: String) throws -> Claims { return Claims( - iat: expiry(days: 1), - exp: defaultIatMilliseconds(), + iat: defaultIatMilliseconds(), + exp: expiry(days: 30), iss: iss, ksu: keyserver.absoluteString, aud: dappUrl, sub: subscriptionAccount.did, - act: "push_subscription" + act: "push_subscription", + scp: scope ) } } + diff --git a/Sources/WalletConnectPush/Types/NotificationConfig.swift b/Sources/WalletConnectPush/Types/NotificationConfig.swift new file mode 100644 index 000000000..c53e9b674 --- /dev/null +++ b/Sources/WalletConnectPush/Types/NotificationConfig.swift @@ -0,0 +1,9 @@ + +import Foundation + +struct NotificationConfig: Codable { + let version: Int + let lastModified: TimeInterval + let types: [NotificationType] + +} diff --git a/Sources/WalletConnectPush/Types/NotificationScope.swift b/Sources/WalletConnectPush/Types/NotificationScope.swift new file mode 100644 index 000000000..b4c74e4be --- /dev/null +++ b/Sources/WalletConnectPush/Types/NotificationScope.swift @@ -0,0 +1,9 @@ + +import Foundation + +public enum NotificationScope: String, Hashable, Codable, CodingKeyRepresentable, CaseIterable { + case promotional + case transactional + case `private` + case alerts +} diff --git a/Sources/WalletConnectPush/Types/NotificationType.swift b/Sources/WalletConnectPush/Types/NotificationType.swift new file mode 100644 index 000000000..0e6b2df35 --- /dev/null +++ b/Sources/WalletConnectPush/Types/NotificationType.swift @@ -0,0 +1,7 @@ + +import Foundation + +public struct NotificationType: Codable, Hashable { + let name: NotificationScope + let description: String +} diff --git a/Sources/WalletConnectPush/Types/PushMessage.swift b/Sources/WalletConnectPush/Types/PushMessage.swift index eb32df560..d48604937 100644 --- a/Sources/WalletConnectPush/Types/PushMessage.swift +++ b/Sources/WalletConnectPush/Types/PushMessage.swift @@ -5,11 +5,13 @@ public struct PushMessage: Codable, Equatable { public let body: String public let icon: String public let url: String + public let type: String? - public init(title: String, body: String, icon: String, url: String) { + public init(title: String, body: String, icon: String, url: String, type: String? = nil) { self.title = title self.body = body self.icon = icon self.url = url + self.type = type } } diff --git a/Sources/WalletConnectPush/Types/PushSubscription.swift b/Sources/WalletConnectPush/Types/PushSubscription.swift index 783ec3dd1..57522f2d8 100644 --- a/Sources/WalletConnectPush/Types/PushSubscription.swift +++ b/Sources/WalletConnectPush/Types/PushSubscription.swift @@ -7,4 +7,11 @@ public struct PushSubscription: Codable, Equatable { public let account: Account public let relay: RelayProtocolOptions public let metadata: AppMetadata + public let scope: [NotificationScope: ScopeValue] + public let expiry: Date +} + +public struct ScopeValue: Codable, Equatable { + let description: String + let enabled: Bool } diff --git a/Sources/WalletConnectPush/Types/WebDidDoc.swift b/Sources/WalletConnectPush/Types/WebDidDoc.swift new file mode 100644 index 000000000..28f301773 --- /dev/null +++ b/Sources/WalletConnectPush/Types/WebDidDoc.swift @@ -0,0 +1,36 @@ + +import Foundation + +// MARK: - WebDidDoc +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 + } +} +extension WebDidDoc { + + struct VerificationMethod: Codable { + let id: String + let type: String + let controller: String + let publicKeyJwk: PublicKeyJwk + } + + struct PublicKeyJwk: Codable { + enum Curve: String, Codable { + case X25519 = "X25519" + } + let kty: String + + let crv: Curve + /// The x member contains the x coordinate for the elliptic curve point. It is represented as the base64url encoding of the coordinate's big endian representation. + let x: String + } +} diff --git a/Sources/WalletConnectRelay/Misc/NetworkError.swift b/Sources/WalletConnectRelay/Misc/NetworkError.swift index 62a384ff0..bb96055a5 100644 --- a/Sources/WalletConnectRelay/Misc/NetworkError.swift +++ b/Sources/WalletConnectRelay/Misc/NetworkError.swift @@ -1,11 +1,16 @@ +import Foundation + enum NetworkError: Error { case webSocketNotConnected case sendMessageFailed(Error) case receiveMessageFailure(Error) } -extension NetworkError { - +extension NetworkError: LocalizedError { + var errorDescription: String? { + return localizedDescription + } + var localizedDescription: String { switch self { case .webSocketNotConnected: diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 7fce1d9ca..90a323c7b 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.5.14"} +{"version": "1.6.0"} diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 59846a852..6831041bb 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -77,7 +77,7 @@ public final class RelayClient { keychainStorage: KeychainStorageProtocol = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk"), socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) ) { let clientIdStorage = ClientIdStorage(keychain: keychainStorage) let socketAuthenticator = SocketAuthenticator( diff --git a/Sources/WalletConnectSign/Namespace.swift b/Sources/WalletConnectSign/Namespace.swift index 5e398757b..43ba9518a 100644 --- a/Sources/WalletConnectSign/Namespace.swift +++ b/Sources/WalletConnectSign/Namespace.swift @@ -1,4 +1,4 @@ -enum AutoNamespacesError: Error { +public enum AutoNamespacesError: Error { case requiredChainsNotSatisfied case requiredAccountsNotSatisfied case requiredMethodsNotSatisfied diff --git a/Sources/WalletConnectUtils/Extensions/Data.swift b/Sources/WalletConnectUtils/Extensions/Data.swift index 88624109f..cb2ffb6c8 100644 --- a/Sources/WalletConnectUtils/Extensions/Data.swift +++ b/Sources/WalletConnectUtils/Extensions/Data.swift @@ -53,4 +53,15 @@ extension Data { public func toHexString() -> String { return map({ String(format: "%02x", $0) }).joined() } + + public init?(base64url: String) { + var base64 = base64url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + + if base64.count % 4 != 0 { + base64.append(String(repeating: "=", count: 4 - base64.count % 4)) + } + self.init(base64Encoded: base64) + } } diff --git a/Sources/WalletConnectVerify/AttestChallengeProvider.swift b/Sources/WalletConnectVerify/AttestChallengeProvider.swift index 5d2891e67..ac9a0eaa3 100644 --- a/Sources/WalletConnectVerify/AttestChallengeProvider.swift +++ b/Sources/WalletConnectVerify/AttestChallengeProvider.swift @@ -6,7 +6,6 @@ protocol AttestChallengeProviding { class AttestChallengeProvider: AttestChallengeProviding { func getChallenge() async throws -> Data { - return Data() fatalError("not implemented") } } diff --git a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift index 1363b0c6a..ef51bd3b4 100644 --- a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift +++ b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift @@ -13,7 +13,7 @@ final class ChatClientProxy { } func request(_ request: RPCRequest) async throws { - guard let event = WebViewEvent(rawValue: request.method) + guard let event = ChatWebViewEvent(rawValue: request.method) else { throw Errors.unregisteredMethod } switch event { diff --git a/Sources/Web3Inbox/ConfigParam.swift b/Sources/Web3Inbox/ConfigParam.swift new file mode 100644 index 000000000..429be268d --- /dev/null +++ b/Sources/Web3Inbox/ConfigParam.swift @@ -0,0 +1,8 @@ + +import Foundation + +public enum ConfigParam { + case chatEnabled + case pushEnabled + case settingsEnabled +} diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift b/Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift new file mode 100644 index 000000000..c64b22f0d --- /dev/null +++ b/Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift @@ -0,0 +1,104 @@ +import Foundation + +final class PushClientProxy { + + private let client: WalletPushClient + + var onSign: SigningCallback + var onResponse: ((RPCResponse) async throws -> Void)? + + init(client: WalletPushClient, onSign: @escaping SigningCallback) { + self.client = client + self.onSign = onSign + } + + func request(_ request: RPCRequest) async throws { + guard let event = PushWebViewEvent(rawValue: request.method) + else { throw Errors.unregisteredMethod } + + switch event { + case .approve: + let params = try parse(ApproveRequest.self, params: request.params) + try await client.approve(id: params.id, onSign: onSign) + try await respond(request: request) + case .update: + let params = try parse(UpdateRequest.self, params: request.params) + try await client.update(topic: params.topic, scope: params.scope) + try await respond(request: request) + case .reject: + let params = try parse(RejectRequest.self, params: request.params) + try await client.reject(id: params.id) + 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, onSign: onSign) + try await respond(request: request) + case .getActiveSubscriptions: + let subscriptions = client.getActiveSubscriptions() + try await respond(with: subscriptions, request: request) + case .getMessageHistory: + let params = try parse(GetMessageHistoryRequest.self, params: request.params) + let messages = client.getMessageHistory(topic: params.topic) + try await respond(with: messages, request: request) + case .deleteSubscription: + let params = try parse(DeleteSubscriptionRequest.self, params: request.params) + try await client.deleteSubscription(topic: params.topic) + try await respond(request: request) + case .deletePushMessage: + let params = try parse(DeletePushMessageRequest.self, params: request.params) + client.deletePushMessage(id: params.id) + try await respond(request: request) + } + } +} + +private extension PushClientProxy { + + private typealias Blob = Dictionary + + enum Errors: Error { + case unregisteredMethod + case unregisteredParams + } + + struct ApproveRequest: Codable { + let id: RPCID + } + + struct UpdateRequest: Codable { + let topic: String + let scope: Set + } + + struct RejectRequest: Codable { + let id: RPCID + } + + struct SubscribeRequest: Codable { + let metadata: AppMetadata + let account: Account + } + + struct GetMessageHistoryRequest: Codable { + let topic: String + } + + struct DeleteSubscriptionRequest: Codable { + let topic: String + } + + struct DeletePushMessageRequest: Codable { + let id: String + } + + func parse(_ type: Request.Type, params: AnyCodable?) throws -> Request { + guard let params = try params?.get(Request.self) + else { throw Errors.unregisteredParams } + return params + } + + func respond(with object: Object = Blob(), request: RPCRequest) async throws { + let response = RPCResponse(matchingRequest: request, result: object) + try await onResponse?(response) + } +} diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift b/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift new file mode 100644 index 000000000..e03e3378c --- /dev/null +++ b/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift @@ -0,0 +1,13 @@ +import Foundation + +enum PushClientRequest: String { + case pushRequest = "push_request" + case pushMessage = "push_message" + case pushUpdate = "push_update" + case pushDelete = "push_delete" + case pushSubscription = "push_subscription" + + var method: String { + return rawValue + } +} diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift b/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift new file mode 100644 index 000000000..7cb07c45a --- /dev/null +++ b/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift @@ -0,0 +1,78 @@ +import Foundation +import Combine + +final class PushClientRequestSubscriber { + + private var publishers: Set = [] + + private let client: WalletPushClient + private let logger: ConsoleLogging + + var onRequest: ((RPCRequest) async throws -> Void)? + + init(client: WalletPushClient, logger: ConsoleLogging) { + self.client = client + self.logger = logger + + setupSubscriptions() + } + + func setupSubscriptions() { + client.requestPublisher.sink { [unowned self] id, account, metadata in + let params = RequestPayload(id: id, account: account, metadata: metadata) + handle(event: .pushRequest, params: params) + }.store(in: &publishers) + + client.pushMessagePublisher.sink { [unowned self] record in + handle(event: .pushMessage, params: record) + }.store(in: &publishers) + + client.deleteSubscriptionPublisher.sink { [unowned self] record in + handle(event: .pushDelete, params: record) + }.store(in: &publishers) + + client.subscriptionPublisher.sink { [unowned self] record in + switch record { + case .success(let subscription): + handle(event: .pushSubscription, params: subscription) + case .failure: + //TODO - handle error + break + + } + }.store(in: &publishers) + + client.updateSubscriptionPublisher.sink { [unowned self] record in + switch record { + case .success(let subscription): + handle(event: .pushUpdate, params: subscription) + case .failure: + //TODO - handle error + break + } + }.store(in: &publishers) + } +} + +private extension PushClientRequestSubscriber { + + struct RequestPayload: Codable { + let id: RPCID + let account: Account + let metadata: AppMetadata + } + + func handle(event: PushClientRequest, params: Codable) { + Task { + do { + let request = RPCRequest( + method: event.method, + params: params + ) + try await onRequest?(request) + } catch { + logger.error("Client Request error: \(error.localizedDescription)") + } + } + } +} diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index 1582c1ed1..32a1a87fb 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -4,13 +4,14 @@ public final class Web3Inbox { /// Web3Inbox client instance public static var instance: Web3InboxClient = { - guard let account, let onSign else { + guard let account, let config = config, let onSign else { fatalError("Error - you must call Web3Inbox.configure(_:) before accessing the shared instance.") } - return Web3InboxClientFactory.create(chatClient: Chat.instance, account: account, onSign: onSign) + return Web3InboxClientFactory.create(chatClient: Chat.instance, pushClient: Push.wallet, account: account, config: config, onSign: onSign) }() private static var account: Account? + private static var config: [ConfigParam: Bool]? private static var onSign: SigningCallback? private init() { } @@ -18,9 +19,11 @@ public final class Web3Inbox { /// Web3Inbox instance config method /// - Parameters: /// - account: Web3Inbox initial account - static public func configure(account: Account, onSign: @escaping SigningCallback) { + static public func configure(account: Account, config: [ConfigParam: Bool] = [:], onSign: @escaping SigningCallback, environment: APNSEnvironment) { Web3Inbox.account = account + Web3Inbox.config = config Web3Inbox.onSign = onSign Chat.configure() + Push.configure(environment: environment) } } diff --git a/Sources/Web3Inbox/Web3InboxClient.swift b/Sources/Web3Inbox/Web3InboxClient.swift index 1fbb15bd4..730180633 100644 --- a/Sources/Web3Inbox/Web3InboxClient.swift +++ b/Sources/Web3Inbox/Web3InboxClient.swift @@ -7,28 +7,42 @@ public final class Web3InboxClient { private var account: Account private let logger: ConsoleLogging - private let clientProxy: ChatClientProxy - private let clientSubscriber: ChatClientRequestSubscriber + private let chatClientProxy: ChatClientProxy + private let chatClientSubscriber: ChatClientRequestSubscriber - private let webviewProxy: WebViewProxy - private let webviewSubscriber: WebViewRequestSubscriber + private let pushClientProxy: PushClientProxy + private let pushClientSubscriber: PushClientRequestSubscriber - init( + private let chatWebviewProxy: WebViewProxy + private let pushWebviewProxy: WebViewProxy + + private let chatWebviewSubscriber: WebViewRequestSubscriber + private let pushWebviewSubscriber: WebViewRequestSubscriber + +init( webView: WKWebView, account: Account, logger: ConsoleLogging, - clientProxy: ChatClientProxy, + chatClientProxy: ChatClientProxy, clientSubscriber: ChatClientRequestSubscriber, - webviewProxy: WebViewProxy, - webviewSubscriber: WebViewRequestSubscriber + chatWebviewProxy: WebViewProxy, + pushWebviewProxy: WebViewProxy, + chatWebviewSubscriber: WebViewRequestSubscriber, + pushWebviewSubscriber: WebViewRequestSubscriber, + pushClientProxy: PushClientProxy, + pushClientSubscriber: PushClientRequestSubscriber ) { self.webView = webView self.account = account self.logger = logger - self.clientProxy = clientProxy - self.clientSubscriber = clientSubscriber - self.webviewProxy = webviewProxy - self.webviewSubscriber = webviewSubscriber + self.chatClientProxy = chatClientProxy + self.chatClientSubscriber = clientSubscriber + self.chatWebviewProxy = chatWebviewProxy + self.pushWebviewProxy = pushWebviewProxy + self.chatWebviewSubscriber = chatWebviewSubscriber + self.pushWebviewSubscriber = pushWebviewSubscriber + self.pushClientProxy = pushClientProxy + self.pushClientSubscriber = pushClientSubscriber setupSubscriptions() } @@ -41,7 +55,7 @@ public final class Web3InboxClient { _ account: Account, onSign: @escaping SigningCallback ) async throws { - clientProxy.onSign = onSign + chatClientProxy.onSign = onSign try await authorize(account: account) } } @@ -51,14 +65,35 @@ public final class Web3InboxClient { private extension Web3InboxClient { func setupSubscriptions() { - webviewSubscriber.onRequest = { [unowned self] request in - try await self.clientProxy.request(request) + + // Chat + + chatClientProxy.onResponse = { [unowned self] response in + try await self.chatWebviewProxy.respond(response) + } + + chatClientSubscriber.onRequest = { [unowned self] request in + try await self.chatWebviewProxy.request(request) + } + + chatWebviewSubscriber.onRequest = { [unowned self] request in + logger.debug("w3i: chat method \(request.method) requested") + try await self.chatClientProxy.request(request) + } + + // Push + + pushClientProxy.onResponse = { [unowned self] response in + try await self.pushWebviewProxy.respond(response) } - clientProxy.onResponse = { [unowned self] response in - try await self.webviewProxy.respond(response) + + pushClientSubscriber.onRequest = { [unowned self] request in + try await self.pushWebviewProxy.request(request) } - clientSubscriber.onRequest = { [unowned self] request in - try await self.webviewProxy.request(request) + + pushWebviewSubscriber.onRequest = { [unowned self] request in + logger.debug("w3i: push method \(request.method) requested") + try await self.pushClientProxy.request(request) } } @@ -69,6 +104,6 @@ private extension Web3InboxClient { method: ChatClientRequest.setAccount.method, params: ["account": account.address] ) - try await webviewProxy.request(request) + try await chatWebviewProxy.request(request) } } diff --git a/Sources/Web3Inbox/Web3InboxClientFactory.swift b/Sources/Web3Inbox/Web3InboxClientFactory.swift index 76c01863c..50d255e29 100644 --- a/Sources/Web3Inbox/Web3InboxClientFactory.swift +++ b/Sources/Web3Inbox/Web3InboxClientFactory.swift @@ -5,29 +5,48 @@ final class Web3InboxClientFactory { static func create( chatClient: ChatClient, + pushClient: WalletPushClient, account: Account, + config: [ConfigParam: Bool], onSign: @escaping SigningCallback ) -> Web3InboxClient { - let host = hostUrlString(account: account) - let logger = ConsoleLogger(suffix: "📬") - let webviewSubscriber = WebViewRequestSubscriber(logger: logger) - let webView = WebViewFactory(host: host, webviewSubscriber: webviewSubscriber).create() - let webViewProxy = WebViewProxy(webView: webView) + let url = buildUrl(account: account, config: config) + let logger = ConsoleLogger(suffix: "📬", loggingLevel: .debug) + let chatWebviewSubscriber = WebViewRequestSubscriber(logger: logger) + let pushWebviewSubscriber = WebViewRequestSubscriber(logger: logger) + let webView = WebViewFactory(url: url, chatWebviewSubscriber: chatWebviewSubscriber, pushWebviewSubscriber: pushWebviewSubscriber).create() + let chatWebViewProxy = WebViewProxy(webView: webView, scriptFormatter: ChatWebViewScriptFormatter(), logger: logger) + let pushWebViewProxy = WebViewProxy(webView: webView, scriptFormatter: PushWebViewScriptFormatter(), logger: logger) + let clientProxy = ChatClientProxy(client: chatClient, onSign: onSign) let clientSubscriber = ChatClientRequestSubscriber(chatClient: chatClient, logger: logger) + let pushClientProxy = PushClientProxy(client: pushClient, onSign: onSign) + let pushClientSubscriber = PushClientRequestSubscriber(client: pushClient, logger: logger) + return Web3InboxClient( webView: webView, account: account, - logger: ConsoleLogger(), - clientProxy: clientProxy, + logger: logger, + chatClientProxy: clientProxy, clientSubscriber: clientSubscriber, - webviewProxy: webViewProxy, - webviewSubscriber: webviewSubscriber + chatWebviewProxy: chatWebViewProxy, + pushWebviewProxy: pushWebViewProxy, + chatWebviewSubscriber: chatWebviewSubscriber, + pushWebviewSubscriber: pushWebviewSubscriber, + pushClientProxy: pushClientProxy, + pushClientSubscriber: pushClientSubscriber ) } - private static func hostUrlString(account: Account) -> String { - return "https://web3inbox-dev-hidden.vercel.app/?chatProvider=ios&account=\(account.address)" + private static func buildUrl(account: Account, config: [ConfigParam: Bool]) -> URL { + var urlComponents = URLComponents(string: "https://web3inbox-dev-hidden.vercel.app/")! + var queryItems = [URLQueryItem(name: "chatProvider", value: "ios"), URLQueryItem(name: "pushProvider", value: "ios"), URLQueryItem(name: "account", value: account.address)] + + for param in config.filter({ $0.value == false}) { + queryItems.append(URLQueryItem(name: "\(param.key)", value: "false")) + } + urlComponents.queryItems = queryItems + return urlComponents.url! } } diff --git a/Sources/Web3Inbox/Web3InboxImports.swift b/Sources/Web3Inbox/Web3InboxImports.swift index 54b421e88..fd78f4977 100644 --- a/Sources/Web3Inbox/Web3InboxImports.swift +++ b/Sources/Web3Inbox/Web3InboxImports.swift @@ -1,3 +1,4 @@ #if !CocoaPods @_exported import WalletConnectChat +@_exported import WalletConnectPush #endif diff --git a/Sources/Web3Inbox/WebView/WebViewEvent.swift b/Sources/Web3Inbox/WebView/ChatWebViewEvent.swift similarity index 86% rename from Sources/Web3Inbox/WebView/WebViewEvent.swift rename to Sources/Web3Inbox/WebView/ChatWebViewEvent.swift index 3556bf3e8..eaf58ea47 100644 --- a/Sources/Web3Inbox/WebView/WebViewEvent.swift +++ b/Sources/Web3Inbox/WebView/ChatWebViewEvent.swift @@ -1,6 +1,6 @@ import Foundation -enum WebViewEvent: String { +enum ChatWebViewEvent: String { case getReceivedInvites case getSentInvites case getThreads diff --git a/Sources/Web3Inbox/WebView/PushWebViewEvent.swift b/Sources/Web3Inbox/WebView/PushWebViewEvent.swift new file mode 100644 index 000000000..5f898fa98 --- /dev/null +++ b/Sources/Web3Inbox/WebView/PushWebViewEvent.swift @@ -0,0 +1,12 @@ +import Foundation + +enum PushWebViewEvent: String { + case approve + case update + case reject + case subscribe + case getActiveSubscriptions + case getMessageHistory + case deleteSubscription + case deletePushMessage +} diff --git a/Sources/Web3Inbox/WebView/WebViewFactory.swift b/Sources/Web3Inbox/WebView/WebViewFactory.swift index bcddd0c9b..70a784c64 100644 --- a/Sources/Web3Inbox/WebView/WebViewFactory.swift +++ b/Sources/Web3Inbox/WebView/WebViewFactory.swift @@ -3,25 +3,36 @@ import WebKit final class WebViewFactory { - private let host: String - private let webviewSubscriber: WebViewRequestSubscriber + private let url: URL + private let chatWebviewSubscriber: WebViewRequestSubscriber + private let pushWebviewSubscriber: WebViewRequestSubscriber - init(host: String, webviewSubscriber: WebViewRequestSubscriber) { - self.host = host - self.webviewSubscriber = webviewSubscriber + init( + url: URL, + chatWebviewSubscriber: WebViewRequestSubscriber, + pushWebviewSubscriber: WebViewRequestSubscriber + ) { + self.url = url + self.chatWebviewSubscriber = chatWebviewSubscriber + self.pushWebviewSubscriber = pushWebviewSubscriber } func create() -> WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.userContentController.add( - webviewSubscriber, - name: WebViewRequestSubscriber.name + chatWebviewSubscriber, + name: WebViewRequestSubscriber.chat + ) + configuration.userContentController.add( + pushWebviewSubscriber, + name: WebViewRequestSubscriber.push ) let webview = WKWebView(frame: .zero, configuration: configuration) - let request = URLRequest(url: URL(string: host)!) + + let request = URLRequest(url: url) webview.load(request) - webview.uiDelegate = webviewSubscriber + webview.uiDelegate = chatWebviewSubscriber return webview } } diff --git a/Sources/Web3Inbox/WebView/WebViewProxy.swift b/Sources/Web3Inbox/WebView/WebViewProxy.swift index 73130fede..5948b6660 100644 --- a/Sources/Web3Inbox/WebView/WebViewProxy.swift +++ b/Sources/Web3Inbox/WebView/WebViewProxy.swift @@ -4,29 +4,47 @@ import WebKit actor WebViewProxy { private let webView: WKWebView + private let scriptFormatter: WebViewScriptFormatter + private let logger: ConsoleLogging - init(webView: WKWebView) { + init(webView: WKWebView, + scriptFormatter: WebViewScriptFormatter, + logger: ConsoleLogging) { self.webView = webView + self.scriptFormatter = scriptFormatter + self.logger = logger } @MainActor func respond(_ response: RPCResponse) async throws { let body = try response.json() - let script = await formatScript(body: body) + logger.debug("resonding to w3i with \(body)") + let script = scriptFormatter.formatScript(body: body) webView.evaluateJavaScript(script, completionHandler: nil) } @MainActor func request(_ request: RPCRequest) async throws { let body = try request.json() - let script = await formatScript(body: body) + logger.debug("requesting w3i with \(body)") + let script = scriptFormatter.formatScript(body: body) webView.evaluateJavaScript(script, completionHandler: nil) } } -private extension WebViewProxy { +protocol WebViewScriptFormatter { + func formatScript(body: String) -> String +} + +class ChatWebViewScriptFormatter: WebViewScriptFormatter { + func formatScript(body: String) -> String { + return "window.web3inbox.chat.postMessage(\(body))" + } +} + +class PushWebViewScriptFormatter: WebViewScriptFormatter { func formatScript(body: String) -> String { - return "window.\(WebViewRequestSubscriber.name).chat.postMessage(\(body))" + return "window.web3inbox.push.postMessage(\(body))" } } diff --git a/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift b/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift index 9b43a57c7..7df7480c4 100644 --- a/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift +++ b/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift @@ -3,7 +3,8 @@ import WebKit final class WebViewRequestSubscriber: NSObject, WKScriptMessageHandler { - static let name = "web3inboxChat" + static let chat = "web3inboxChat" + static let push = "web3inboxPush" var onRequest: ((RPCRequest) async throws -> Void)? @@ -17,13 +18,12 @@ final class WebViewRequestSubscriber: NSObject, WKScriptMessageHandler { _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { - guard message.name == WebViewRequestSubscriber.name else { return } - + logger.debug("WebViewRequestSubscriber: received request from w3i") guard let body = message.body as? String, let data = body.data(using: .utf8), let request = try? JSONDecoder().decode(RPCRequest.self, from: data) else { return } - + logger.debug("request method: \(request.method)") Task { do { try await onRequest?(request) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3c19c1be9..b7e71fc34 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -20,16 +20,35 @@ default_platform(:ios) platform :ios do lane :tests do |options| + xcargs = "" + xcargs << "RELAY_HOST='#{options[:relay_host]}' " if options[:relay_host] + xcargs << "PROJECT_ID='#{options[:project_id]}' " if options[:project_id] + + xctestrun = "" + Dir.chdir("..") do + xctestrun = Dir.glob("**/*#{options[:testplan]}*.xctestrun").first + + puts "Using following xctestrun file:" + puts xctestrun + end + run_tests( project: 'Example/ExampleApp.xcodeproj', scheme: options[:scheme], - testplan: options[:testplan], + testplan: defined?(xctestrun) ? nil : options[:testplan], cloned_source_packages_path: 'SourcePackagesCache', - destination: 'platform=iOS Simulator,name=iPhone 13', + destination: 'platform=iOS Simulator,name=iPhone 14', derived_data_path: 'DerivedDataCache', skip_package_dependencies_resolution: true, skip_build: true, - xcargs: "RELAY_HOST='#{options[:relay_host]}' PROJECT_ID='#{options[:project_id]}'" + xcargs: xcargs, + xcodebuild_formatter: "xcbeautify --preserve-unbeautified", + output_directory: "test_results", + result_bundle: true, + buildlog_path: "test_results", + output_types: "junit", + xctestrun: xctestrun, + test_without_building: true, ) end @@ -37,7 +56,7 @@ platform :ios do xcodebuild( project: 'Example/ExampleApp.xcodeproj', scheme: options[:scheme], - destination: 'platform=iOS Simulator,name=iPhone 13', + destination: 'platform=iOS Simulator,name=iPhone 14', xcargs: "-clonedSourcePackagesDirPath SourcePackagesCache -derivedDataPath DerivedDataCache" ) end @@ -67,7 +86,9 @@ platform :ios do git_url: "https://github.com/WalletConnect/match-swift.git", git_basic_authorization: options[:token], api_key: api_key, - include_all_certificates: true + include_all_certificates: true, + force_for_new_devices: true, + force_for_new_certificates: true ) number = latest_testflight_build_number( app_identifier: ENV["APP_IDENTIFIER"],