diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 374c82bdef..905c365582 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -15,7 +15,7 @@ env: jobs: build: name: Build - runs-on: macos-12 + runs-on: macos-14 # Concurrency group not needed as this workflow only runs on develop which we always want to test. diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index ba69c463e3..5ffde41ac8 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -16,7 +16,7 @@ env: jobs: tests: name: Tests - runs-on: macos-12 + runs-on: macos-14 concurrency: # When running on develop, use the sha to allow all runs of this workflow to run concurrently. diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml new file mode 100644 index 0000000000..b13272947f --- /dev/null +++ b/.github/workflows/ci-ui-tests.yml @@ -0,0 +1,63 @@ +name: UI Tests CI + +on: + pull_request: + + workflow_dispatch: + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + tests: + name: UI Tests + runs-on: macos-14 + + concurrency: + # Only allow a single run of this workflow on each branch, automatically cancelling older runs. + group: ui-tests-${{ github.head_ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixSDK + - name: Reset MatrixSDK pod + run: rm -rf Pods/MatrixSDK + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Main step + - name: UI tests + run: bundle exec fastlane uitest + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + flags: uitests + \ No newline at end of file diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml new file mode 100644 index 0000000000..e610628b45 --- /dev/null +++ b/.github/workflows/release-alpha.yml @@ -0,0 +1,95 @@ +name: Build alpha release + +on: + + # Triggers the workflow on any pull request + pull_request: + types: [ labeled, synchronize, opened, reopened ] + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + build: + # Only run for PRs that contain the trigger label. The action will fail for forks due to + # missing secrets, but there's no need to handle this as it won't run automatically. + if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build') + + name: Release + runs-on: macos-14 + + concurrency: + # Only allow a single run of this workflow on each branch, automatically cancelling older runs. + group: alpha-${{ github.head_ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Cache CocoaPods libraries + uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + + - name: Cache Ruby gems + uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixSDK + - name: Reset MatrixSDK pod + run: rm -rf Pods/MatrixSDK + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Import alpha release private signing certificate + - name: Import signing certificate + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.ALPHA_CERTIFICATES_P12 }} + p12-password: ${{ secrets.ALPHA_CERTIFICATES_P12_PASSWORD }} + + # Main step + # The Ad-hoc release link will be referenced as 'DIAWI_FILE_LINK' + # and QR link as 'DIAWI_QR_CODE_LINK' when the Diawi upload succeed + - name: Build Ad-hoc release and send it to Diawi + run: bundle exec fastlane alpha + env: + APPSTORECONNECT_KEY_ID: ${{ secrets.APPSTORECONNECT_KEY_ID }} + APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.APPSTORECONNECT_KEY_ISSUER_ID }} + APPSTORECONNECT_KEY_CONTENT: ${{ secrets.APPSTORECONNECT_KEY_CONTENT }} + DIAWI_API_TOKEN: ${{ secrets.DIAWI_API_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Add or update PR comment with Ad-hoc release informations + uses: NejcZdovc/comment-pr@v1 + with: + message: | + :iphone: Scan the QR code below to install the build for this PR. + :lock: This build is for internal testing purpose. Only devices listed in the ad-hoc provisioning profile can install Element Alpha. + + ![QR code](${{ env.DIAWI_QR_CODE_LINK }}) + + If you can't scan the QR code you can install the build via this link: ${{ env.DIAWI_FILE_LINK }} + # Enables to identify and update existing Ad-hoc release message on new commit in the PR + identifier: "GITHUB_COMMENT_ADHOC_RELEASE" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000000..d902ca1368 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,31 @@ +name: SonarCloud analysis + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + workflow_dispatch: + +permissions: + pull-requests: read # allows SonarCloud to decorate PRs with analysis results + +jobs: + Analysis: + runs-on: ubuntu-latest + + steps: + - name: Analyze with SonarCloud + + # You can pin the exact commit or the version. + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate the token on Sonarcloud.io, add it to the secrets of this repo + with: + # Additional arguments for the sonarcloud scanner + args: + -Dsonar.projectKey=vector-im_element-ios + -Dsonar.organization=new_vector_ltd_organization + -Dsonar.inclusions=RiotSwiftUI/** + # For more info about the parameters, please refer to https://docs.sonarcloud.io/advanced-setup/analysis-parameters/ \ No newline at end of file diff --git a/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy index c3b45f6413..500ae9affe 100644 --- a/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy +++ b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/Btchap/Generated/InfoPlist.swift b/Btchap/Generated/InfoPlist.swift index d65235e8cb..fd4913b7ce 100644 --- a/Btchap/Generated/InfoPlist.swift +++ b/Btchap/Generated/InfoPlist.swift @@ -30,6 +30,8 @@ internal enum InfoPlist { internal static let nsCameraUsageDescription: String = _document["NSCameraUsageDescription"] internal static let nsContactsUsageDescription: String = _document["NSContactsUsageDescription"] internal static let nsFaceIDUsageDescription: String = _document["NSFaceIDUsageDescription"] + internal static let nsLocationAlwaysAndWhenInUseUsageDescription: String = _document["NSLocationAlwaysAndWhenInUseUsageDescription"] + internal static let nsLocationWhenInUseUsageDescription: String = _document["NSLocationWhenInUseUsageDescription"] internal static let nsMicrophoneUsageDescription: String = _document["NSMicrophoneUsageDescription"] internal static let nsPhotoLibraryUsageDescription: String = _document["NSPhotoLibraryUsageDescription"] internal static let uiBackgroundModes: [String] = _document["UIBackgroundModes"] diff --git a/CHANGES.md b/CHANGES.md index 174be9ac8a..eb5267b66d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +## Changes in 1.11.9 (2024-04-02) + +Others + +- Update matrix-analytics-events to version 0.15.0 ([#7768](https://github.com/element-hq/element-ios/pull/7768)) +- Upgrade to build with Xcode 15.2 +- Add a privacy manifest + + ## Changes in 1.11.8 (2024-03-05) 🙌 Improvements diff --git a/DevTchap/Generated/InfoPlist.swift b/DevTchap/Generated/InfoPlist.swift index c2b39bfa29..2306b5b8c2 100644 --- a/DevTchap/Generated/InfoPlist.swift +++ b/DevTchap/Generated/InfoPlist.swift @@ -29,6 +29,8 @@ internal enum InfoPlist { internal static let nsCameraUsageDescription: String = _document["NSCameraUsageDescription"] internal static let nsContactsUsageDescription: String = _document["NSContactsUsageDescription"] internal static let nsFaceIDUsageDescription: String = _document["NSFaceIDUsageDescription"] + internal static let nsLocationAlwaysAndWhenInUseUsageDescription: String = _document["NSLocationAlwaysAndWhenInUseUsageDescription"] + internal static let nsLocationWhenInUseUsageDescription: String = _document["NSLocationWhenInUseUsageDescription"] internal static let nsMicrophoneUsageDescription: String = _document["NSMicrophoneUsageDescription"] internal static let nsPhotoLibraryUsageDescription: String = _document["NSPhotoLibraryUsageDescription"] internal static let uiBackgroundModes: [String] = _document["UIBackgroundModes"] diff --git a/Gemfile.lock b/Gemfile.lock index 02bb363639..519e1f087e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,9 +7,11 @@ GIT GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.1.2) + activesupport (7.1.3.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -19,32 +21,32 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.859.0) - aws-sdk-core (3.188.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (1.899.0) + aws-sdk-core (3.191.4) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.73.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.140.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-s3 (1.146.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.7.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) - bigdecimal (3.1.4) + bigdecimal (3.1.7) claide (1.1.0) clamp (1.3.2) cocoapods (1.14.3) @@ -88,20 +90,19 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.0) - ruby2_keywords + drb (2.2.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.104.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -130,8 +131,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -150,6 +151,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -158,7 +160,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -172,7 +174,7 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-brew (0.1.1) - fastlane-plugin-sentry (1.16.0) + fastlane-plugin-sentry (1.20.0) os (~> 1.1, >= 1.1.4) fastlane-plugin-versioning (0.5.2) fastlane-plugin-xcodegen (1.1.0) @@ -181,9 +183,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.53.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -191,24 +193,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -222,29 +223,31 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) + json (2.7.1) + jwt (2.8.1) + base64 mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.22.3) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - nokogiri (1.15.5) + nkf (0.2.0) + nokogiri (1.15.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) - optparse (0.1.1) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) + plist (3.7.1) public_suffix (4.0.7) racc (1.7.3) rake (13.1.0) @@ -259,7 +262,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.18.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -278,7 +281,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) typhoeus (1.4.1) @@ -287,12 +290,11 @@ GEM concurrent-ruby (~> 1.0) uber (0.1.0) unicode-display_width (2.5.0) - webrick (1.8.1) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2f19e2ddc5..511ea29a7b 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { - "revision" : "2f5fa5f1e2f6c6ae1a47c33d953a3ce289167eb0", - "version" : "0.5.0" + "revision" : "44d5a0e898a71f8abbbe12afe9d73e82d370a9a1", + "version" : "0.15.0" } }, { diff --git a/Riot/Assets/be.lproj/Vector.strings b/Riot/Assets/be.lproj/Vector.strings index 8b13789179..64295bad49 100644 --- a/Riot/Assets/be.lproj/Vector.strings +++ b/Riot/Assets/be.lproj/Vector.strings @@ -1 +1,9 @@ + + +// Titles +"title_home" = "Галоўная"; +"people_empty_view_title" = "Удзельнікі"; +"group_details_home" = "Галоўная"; +"spaces_home_space_title" = "Галоўная"; +"title_people" = "Удзельнікі"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 3df4d23b9b..22e9f34192 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1889,8 +1889,8 @@ "notice_conference_call_started" = "VoIP rühmakõne algas"; "notice_conference_call_finished" = "VoIP rühmakõne lõppes"; // Notice Events with "You" -"notice_room_invite_by_you" = "Sina kutsusid kasutajat %@"; -"notice_room_invite_you" = "%@ kutsus sind"; +"notice_room_invite_by_you" = "Sina saatsid kutse kasutajale %@"; +"notice_room_invite_you" = "%@ saatis sulle kutse"; "notice_room_third_party_invite_by_you" = "Sina saatsid kasutajale %@ kutse jututoaga liitumiseks"; "notice_room_third_party_registered_invite_by_you" = "Sina võtsid vastu kutse %@ nimel"; "notice_room_third_party_revoked_invite_by_you" = "Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %@"; @@ -2025,7 +2025,7 @@ "notice_room_third_party_invite_for_dm" = "%@ saatis kutse kasutajale %@"; "notice_room_third_party_revoked_invite_for_dm" = "%@ võttis tagasi kasutaja %@ kutse"; "notice_room_name_changed_for_dm" = "%@ muutis jututoa uueks nimeks %@."; -"notice_room_third_party_invite_by_you_for_dm" = "Sina kutsusid kasutajat %@"; +"notice_room_third_party_invite_by_you_for_dm" = "Sina saatsid kutse kasutajale %@"; "notice_room_third_party_revoked_invite_by_you_for_dm" = "Sina võtsid tagasi kasutaja %@ kutse"; "notice_room_name_changed_by_you_for_dm" = "Sa muutsid jututoa uueks nimeks %@."; "notice_room_name_removed_by_you_for_dm" = "Sa eemaldasid jututoa nime"; diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 1a30841b98..d9d68e2f85 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -213,6 +213,25 @@ import AnalyticsEvents } } +@objc +protocol E2EAnalytics { + func trackE2EEError(_ failure: DecryptionFailure) +} + + +@objc extension Analytics: E2EAnalytics { + + /// Track an E2EE error that occurred + /// - Parameters: + /// - reason: The error that occurred. + /// - context: Additional context of the error that occured + func trackE2EEError(_ failure: DecryptionFailure) { + let event = failure.toAnalyticsEvent() + capture(event: event) + } + +} + // MARK: - Public tracking methods // The following methods are exposed for compatibility with Objective-C as // the `capture` method and the generated events cannot be bridged from Swift. @@ -266,20 +285,7 @@ extension Analytics { func trackInteraction(_ uiElement: AnalyticsUIElement) { trackInteraction(uiElement, interactionType: .Touch, index: nil) } - - /// Track an E2EE error that occurred - /// - Parameters: - /// - reason: The error that occurred. - /// - context: Additional context of the error that occured - func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { - let event = AnalyticsEvent.Error( - context: context, - cryptoModule: .Rust, - domain: .E2EE, - name: reason.errorName - ) - capture(event: event) - } + /// Track when a user becomes unauthenticated without pressing the `sign out` button. /// - Parameters: @@ -355,7 +361,8 @@ extension Analytics: MXAnalyticsDelegate { func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming) - let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, domain: .VOIP, name: reason.errorName) + let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, cryptoSDK: nil, domain: .VOIP, eventLocalAgeMillis: nil, + isFederated: nil, isMatrixDotOrg: nil, name: reason.errorName, timeToDecryptMillis: nil, userTrustsOwnIdentity: nil, wasVisibleToUser: nil) capture(event: callEvent) capture(event: event) } @@ -386,6 +393,7 @@ extension Analytics: MXAnalyticsDelegate { let event = AnalyticsEvent.Composer(inThread: inThread, isEditing: isEditing, isReply: isReply, + messageType: .Text, startsThread: startsThread) capture(event: event) } diff --git a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift new file mode 100644 index 0000000000..cd129aee35 --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift @@ -0,0 +1,44 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AnalyticsEvents + +extension DecryptionFailure { + + public func toAnalyticsEvent() -> AnalyticsEvent.Error { + + let timeToDecryptMillis: Int = if self.timeToDecrypt != nil { + Int(self.timeToDecrypt! * 1000) + } else { + -1 + } + return AnalyticsEvent.Error( + context: self.context, + cryptoModule: .Rust, + cryptoSDK: .Rust, + domain: .E2EE, + + eventLocalAgeMillis: nil, + isFederated: nil, + isMatrixDotOrg: nil, + name: self.reason.errorName, + timeToDecryptMillis: timeToDecryptMillis, + userTrustsOwnIdentity: nil, + wasVisibleToUser: nil + ) + } +} diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 1c991db88f..9e0ca57858 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -38,15 +38,19 @@ import AnalyticsEvents /// The id of the event that was unabled to decrypt. let failedEventId: String /// The time the failure has been reported. - let ts: TimeInterval = Date().timeIntervalSince1970 + let ts: TimeInterval /// Decryption failure reason. let reason: DecryptionFailureReason /// Additional context of failure let context: String - init(failedEventId: String, reason: DecryptionFailureReason, context: String) { + /// UTDs can be permanent or temporary. If temporary, this field will contain the time it took to decrypt the message in milliseconds. If permanent should be nil + var timeToDecrypt: TimeInterval? + + init(failedEventId: String, reason: DecryptionFailureReason, context: String, ts: TimeInterval) { self.failedEventId = failedEventId self.reason = reason self.context = context + self.ts = ts } } diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.h b/Riot/Modules/Analytics/DecryptionFailureTracker.h deleted file mode 100644 index b8f9ca467e..0000000000 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2018 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import - -@class DecryptionFailureTracker; - -@class Analytics; -@import MatrixSDK; - -@interface DecryptionFailureTracker : NSObject - -/** - Returns the shared tracker. - - @return the shared tracker. - */ -+ (instancetype)sharedInstance; - -/** - The delegate object to receive analytics events. - */ -@property (nonatomic, weak) Analytics *delegate; - -/** - Report an event unable to decrypt. - - This error can be momentary. The DecryptionFailureTracker will check if it gets - fixed. Else, it will generate a failure (@see `trackFailures`). - - @param event the event. - @param roomState the room state when the event was received. - @param userId my user id. - */ -- (void)reportUnableToDecryptErrorForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState myUser:(NSString*)userId; - -/** - Flush current data. - */ -- (void)dispatch; - -@end diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m deleted file mode 100644 index 4a749b71aa..0000000000 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ /dev/null @@ -1,174 +0,0 @@ -/* - Copyright 2018 New Vector Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -#import "DecryptionFailureTracker.h" -#import "GeneratedInterface-Swift.h" - - -// Call `checkFailures` every `CHECK_INTERVAL` -#define CHECK_INTERVAL 2 - -// Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting -// and reporting them as failures -#define GRACE_PERIOD 4 - -// E2E failures analytics category. -NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; - -@interface DecryptionFailureTracker() -{ - // Reported failures - // Every `CHECK_INTERVAL`, this list is checked for failures that happened - // more than`GRACE_PERIOD` ago. Those that did are reported to the delegate. - NSMutableDictionary *reportedFailures; - - // Event ids of failures that were tracked previously - NSMutableSet *trackedEvents; - - // Timer for periodic check - NSTimer *checkFailuresTimer; -} -@end - -@implementation DecryptionFailureTracker - -+ (instancetype)sharedInstance -{ - static DecryptionFailureTracker *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[DecryptionFailureTracker alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - if (self) - { - reportedFailures = [NSMutableDictionary dictionary]; - trackedEvents = [NSMutableSet set]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil]; - - checkFailuresTimer = [NSTimer scheduledTimerWithTimeInterval:CHECK_INTERVAL - target:self - selector:@selector(checkFailures) - userInfo:nil - repeats:YES]; - } - return self; -} - -- (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState myUser:(NSString *)userId -{ - if (reportedFailures[event.eventId] || [trackedEvents containsObject:event.eventId]) - { - return; - } - - // Filter out "expected" UTDs - // We cannot decrypt messages sent before the user joined the room - MXRoomMember *myUser = [roomState.members memberWithUserId:userId]; - if (!myUser || myUser.membership != MXMembershipJoin) - { - return; - } - - NSString *failedEventId = event.eventId; - DecryptionFailureReason reason; - - // Categorise the error - switch (event.decryptionError.code) - { - case MXDecryptingErrorUnknownInboundSessionIdCode: - reason = DecryptionFailureReasonOlmKeysNotSent; - break; - - case MXDecryptingErrorOlmCode: - reason = DecryptionFailureReasonOlmIndexError; - break; - - default: - // All other error codes will be tracked as `OlmUnspecifiedError` and will include `context` containing - // the actual error code and localized description - reason = DecryptionFailureReasonUnspecified; - break; - } - - NSString *context = [NSString stringWithFormat:@"code: %ld, description: %@", event.decryptionError.code, event.decryptionError.localizedDescription]; - reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId - reason:reason - context:context]; -} - -- (void)dispatch -{ - [self checkFailures]; -} - -#pragma mark - Private methods - -/** - Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be - tracked. - */ -- (void)checkFailures -{ - if (!_delegate) - { - return; - } - - NSTimeInterval tsNow = [NSDate date].timeIntervalSince1970; - - NSMutableArray *failuresToTrack = [NSMutableArray array]; - - for (DecryptionFailure *reportedFailure in reportedFailures.allValues) - { - if (reportedFailure.ts < tsNow - GRACE_PERIOD) - { - [failuresToTrack addObject:reportedFailure]; - [reportedFailures removeObjectForKey:reportedFailure.failedEventId]; - [trackedEvents addObject:reportedFailure.failedEventId]; - } - } - - if (failuresToTrack.count) - { - // Sort failures by error reason - NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; - for (DecryptionFailure *failure in failuresToTrack) - { - failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); - [self.delegate trackE2EEError:failure.reason context:failure.context]; - } - - MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); - } -} - -- (void)eventDidDecrypt:(NSNotification *)notif -{ - // Could be an event in the reportedFailures, remove it - MXEvent *event = notif.object; - [reportedFailures removeObjectForKey:event.eventId]; -} - -@end diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.swift b/Riot/Modules/Analytics/DecryptionFailureTracker.swift new file mode 100644 index 0000000000..19b8afb19c --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.swift @@ -0,0 +1,174 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +// Protocol to get the current time. Used for easy testing +protocol TimeProvider { + func nowTs() -> TimeInterval +} + +class DefaultTimeProvider: TimeProvider { + + func nowTs() -> TimeInterval { + return Date.now.timeIntervalSince1970 + } + +} + + +@objc +class DecryptionFailureTracker: NSObject { + + let GRACE_PERIOD: TimeInterval = 4 + // Call `checkFailures` every `CHECK_INTERVAL` + let CHECK_INTERVAL: TimeInterval = 15 + + // The maximum time to wait for a late decryption before reporting as permanent UTD + let MAX_WAIT_FOR_LATE_DECRYPTION: TimeInterval = 60 + + @objc weak var delegate: E2EAnalytics? + + // Reported failures + var reportedFailures = [String /* eventId */: DecryptionFailure]() + + // Event ids of failures that were tracked previously + var trackedEvents = Set() + + var checkFailuresTimer: Timer? + + @objc static let sharedInstance = DecryptionFailureTracker() + + var timeProvider: TimeProvider = DefaultTimeProvider() + + override init() { + super.init() + + NotificationCenter.default.addObserver(self, + selector: #selector(eventDidDecrypt(_:)), + name: .mxEventDidDecrypt, + object: nil) + + } + + @objc + func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, myUser userId: String) { + if reportedFailures[event.eventId] != nil || trackedEvents.contains(event.eventId) { + return + } + + // Filter out "expected" UTDs + // We cannot decrypt messages sent before the user joined the room + guard let myUser = roomState.members.member(withUserId: userId) else { return } + if myUser.membership != MXMembership.join { + return + } + + guard let failedEventId = event.eventId else { return } + + guard let error = event.decryptionError as? NSError else { return } + + var reason = DecryptionFailureReason.unspecified + + if error.code == MXDecryptingErrorUnknownInboundSessionIdCode.rawValue { + reason = DecryptionFailureReason.olmKeysNotSent + } else if error.code == MXDecryptingErrorOlmCode.rawValue { + reason = DecryptionFailureReason.olmIndexError + } + + let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription) + + reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs()) + + // Start the ticker if needed. There is no need to have a ticker if no failures are tracked + if checkFailuresTimer == nil { + self.checkFailuresTimer = Timer.scheduledTimer(withTimeInterval: CHECK_INTERVAL, repeats: true) { [weak self] _ in + self?.checkFailures() + } + } + + } + + @objc + func dispatch() { + self.checkFailures() + } + + @objc + func eventDidDecrypt(_ notification: Notification) { + guard let event = notification.object as? MXEvent else { return } + + guard let reportedFailure = self.reportedFailures[event.eventId] else { return } + + let now = self.timeProvider.nowTs() + let ellapsedTime = now - reportedFailure.ts + + if ellapsedTime < 4 { + // event is graced + reportedFailures.removeValue(forKey: event.eventId) + } else { + // It's a late decrypt must be reported as a late decrypt + reportedFailure.timeToDecrypt = ellapsedTime + self.delegate?.trackE2EEError(reportedFailure) + } + // Remove from reported failures + self.trackedEvents.insert(event.eventId) + reportedFailures.removeValue(forKey: event.eventId) + + // Check if we still need the ticker timer + if reportedFailures.isEmpty { + // Invalidate the current timer, nothing to check for + self.checkFailuresTimer?.invalidate() + self.checkFailuresTimer = nil + } + + } + + /** + Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be + tracked. + */ + @objc + func checkFailures() { + guard let delegate = self.delegate else {return} + + let tsNow = self.timeProvider.nowTs() + var failuresToCheck = [DecryptionFailure]() + + for reportedFailure in self.reportedFailures.values { + let ellapsed = tsNow - reportedFailure.ts + if ellapsed > MAX_WAIT_FOR_LATE_DECRYPTION { + failuresToCheck.append(reportedFailure) + reportedFailure.timeToDecrypt = nil + reportedFailures.removeValue(forKey: reportedFailure.failedEventId) + trackedEvents.insert(reportedFailure.failedEventId) + } + } + + for failure in failuresToCheck { + delegate.trackE2EEError(failure) + } + + // Check if we still need the ticker timer + if reportedFailures.isEmpty { + // Invalidate the current timer, nothing to check for + self.checkFailuresTimer?.invalidate() + self.checkFailuresTimer = nil + } + } + +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index db266e53f0..1011a4f832 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -33,7 +33,6 @@ //#import "ContactDetailsViewController.h" #import "BugReportViewController.h" -#import "DecryptionFailureTracker.h" #import "Tools.h" #import "WidgetManager.h" diff --git a/Riot/SupportingFiles/PrivacyInfo.xcprivacy b/Riot/SupportingFiles/PrivacyInfo.xcprivacy index c3b45f6413..500ae9affe 100644 --- a/Riot/SupportingFiles/PrivacyInfo.xcprivacy +++ b/Riot/SupportingFiles/PrivacyInfo.xcprivacy @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 6db1716ba2..d456343ea1 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -23,7 +23,6 @@ #import "WidgetManager.h" #import "MXDecryptionResult.h" -#import "DecryptionFailureTracker.h" #import diff --git a/RiotNSE/BuildSettings.swift b/RiotNSE/BuildSettings.swift index 0c5d9ff411..ce637a7d40 100644 --- a/RiotNSE/BuildSettings.swift +++ b/RiotNSE/BuildSettings.swift @@ -270,7 +270,7 @@ final class BuildSettings: NSObject { static let tchapFeatureGeolocationSharing = "tchapFeatureGeolocationSharing" // linked to `locationSharingEnabled` property (see above) static var tchapFeaturesAllowedHomeServersForFeature: [String: [String]] = [ tchapFeatureNotificationByEmail: [ - "agent.dinum.tchap.gouv.fr" + tchapFeatureAnyHomeServer ], tchapFeatureVoiceOverIP: [ "agent.dinum.tchap.gouv.fr" @@ -456,7 +456,12 @@ final class BuildSettings: NSObject { // MARK: - Location Sharing /// Overwritten by the home server's .well-known configuration (if any exists) - static let defaultTileServerMapStyleURL = URL(string: "https://openmaptiles.geo.data.gouv.fr/styles/osm-bright/style.json")! + // Tchap: handle different map providers. + private enum TchapMapProvider: String { + case geoDataGouv = "https://openmaptiles.geo.data.gouv.fr/styles/osm-bright/style.json" + case ign = "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json" + } + static let defaultTileServerMapStyleURL = URL(string: TchapMapProvider.geoDataGouv.rawValue)! static let locationSharingEnabled = true diff --git a/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy index c3b45f6413..500ae9affe 100644 --- a/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy +++ b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/RiotShareExtension/BuildSettings.swift b/RiotShareExtension/BuildSettings.swift index 0c5d9ff411..ce637a7d40 100644 --- a/RiotShareExtension/BuildSettings.swift +++ b/RiotShareExtension/BuildSettings.swift @@ -270,7 +270,7 @@ final class BuildSettings: NSObject { static let tchapFeatureGeolocationSharing = "tchapFeatureGeolocationSharing" // linked to `locationSharingEnabled` property (see above) static var tchapFeaturesAllowedHomeServersForFeature: [String: [String]] = [ tchapFeatureNotificationByEmail: [ - "agent.dinum.tchap.gouv.fr" + tchapFeatureAnyHomeServer ], tchapFeatureVoiceOverIP: [ "agent.dinum.tchap.gouv.fr" @@ -456,7 +456,12 @@ final class BuildSettings: NSObject { // MARK: - Location Sharing /// Overwritten by the home server's .well-known configuration (if any exists) - static let defaultTileServerMapStyleURL = URL(string: "https://openmaptiles.geo.data.gouv.fr/styles/osm-bright/style.json")! + // Tchap: handle different map providers. + private enum TchapMapProvider: String { + case geoDataGouv = "https://openmaptiles.geo.data.gouv.fr/styles/osm-bright/style.json" + case ign = "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json" + } + static let defaultTileServerMapStyleURL = URL(string: TchapMapProvider.geoDataGouv.rawValue)! static let locationSharingEnabled = true diff --git a/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift b/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift new file mode 100644 index 0000000000..db41a603ef --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift @@ -0,0 +1,28 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +extension XCUIElement { + func forceTap() { + if isHittable { + tap() + } else { + let coordinate: XCUICoordinate = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.5)) + coordinate.tap() + } + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index 95b5e08fad..b241dcfcee 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -41,37 +41,6 @@ final class NotificationSettingsViewModelTests: XCTestCase { XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false) } - func testUpdateOneToOneRuleAlsoUpdatesPollRules() async { - setupWithPollRules() - - await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) - - XCTAssertEqual(viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], false) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], false) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) - XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], true) - XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], true) - } - - func testUpdateMessageRuleAlsoUpdatesPollRules() async { - setupWithPollRules() - - await viewModel.update(ruleID: .allOtherMessages, isChecked: false) - XCTAssertEqual(viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], false) - XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], false) - XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], true) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], true) - } - func testMismatchingRulesAreHandled() async { setupWithPollRules() diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift index 71a6516592..3fcf2a8ccd 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -56,7 +56,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsMoreMenuButtonSelected_moreMenuIsCorrect() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() XCTAssertTrue(app.buttons["Select sessions"].exists) XCTAssertTrue(app.buttons["Sign out of 6 sessions"].exists) } @@ -64,7 +64,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsSelectSessionsSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() let signOutButton = app.buttons["Sign out"] XCTAssertTrue(signOutButton.exists) @@ -76,7 +76,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsSelectAllSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() app.buttons["Select All"].tap() XCTAssertTrue(app.buttons["Deselect All"].exists) @@ -85,7 +85,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenAllOtherSessionsAreSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() for i in 0...MockUserOtherSessionsScreenState.all.allSessions().count - 1 { app.buttons["UserSessionListItem_\(i)"].tap() @@ -95,7 +95,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenChangingSessionSelection_signOutButtonChangesItState() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() let signOutButton = app.buttons["Sign out"] XCTAssertTrue(signOutButton.exists) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift index ba844904ec..39970240d9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift @@ -80,35 +80,33 @@ struct UserOtherSessionsToolbar: ToolbarContent { } private func optionsMenu() -> some View { - Button { } label: { - Menu { - if showDeviceLogout { // As you can only sign out the selected sessions, we don't allow selection when you're unable to sign out devices. - Button { - isEditModeEnabled = true - } label: { - Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") - } - .disabled(sessionCount == 0) - } - + Menu { + if showDeviceLogout { // As you can only sign out the selected sessions, we don't allow selection when you're unable to sign out devices. Button { - isShowLocationEnabled.toggle() + isEditModeEnabled = true } label: { - Label(showLocationInfo: isShowLocationEnabled) - } - - if sessionCount > 0, showDeviceLogout { - DestructiveButton { - onSignOut() - } label: { - Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(sessionCount)), systemImage: "rectangle.portrait.and.arrow.forward.fill") - } + Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") } + .disabled(sessionCount == 0) + } + + Button { + isShowLocationEnabled.toggle() } label: { - Image(systemName: "ellipsis") - .padding(.horizontal, 4) - .padding(.vertical, 12) + Label(showLocationInfo: isShowLocationEnabled) + } + + if sessionCount > 0, showDeviceLogout { + DestructiveButton { + onSignOut() + } label: { + Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(sessionCount)), systemImage: "rectangle.portrait.and.arrow.forward.fill") + } } + } label: { + Image(systemName: "ellipsis") + .padding(.horizontal, 4) + .padding(.vertical, 12) } .accessibilityIdentifier("More") } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift index 643c28cbe9..93938057ca 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift @@ -67,7 +67,7 @@ class UserSessionOverviewUITests: MockScreenTestCase { let navTitle = VectorL10n.userSessionOverviewSessionTitle let barButton = app.navigationBars[navTitle].buttons["Menu"] XCTAssertTrue(barButton.exists) - barButton.tap() + barButton.forceTap() XCTAssertTrue(app.buttons[VectorL10n.signOut].exists) XCTAssertTrue(app.buttons[VectorL10n.manageSessionRename].exists) } diff --git a/RiotTests/DecryptionFailureTrackerTests.swift b/RiotTests/DecryptionFailureTrackerTests.swift new file mode 100644 index 0000000000..7cd9bf480f --- /dev/null +++ b/RiotTests/DecryptionFailureTrackerTests.swift @@ -0,0 +1,341 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +import XCTest +@testable import Element + + +class DecryptionFailureTrackerTests: XCTestCase { + + class TimeShifter: TimeProvider { + + var timestamp = TimeInterval(0) + + func nowTs() -> TimeInterval { + return timestamp + } + } + + class AnalyticsDelegate : E2EAnalytics { + var reportedFailure: Element.DecryptionFailure?; + + func trackE2EEError(_ reason: Element.DecryptionFailure) { + reportedFailure = reason + } + + } + + let timeShifter = TimeShifter() + + func test_grace_period() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + timeShifter.timestamp = TimeInterval(2) + + // simulate decrypted in the grace period + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + + // Pass the grace period + timeShifter.timestamp = TimeInterval(5) + + decryptionFailureTracker.checkFailures(); + XCTAssertNil(testDelegate.reportedFailure); + + } + + func test_report_ratcheted_key_utd() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorOlmCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the max period + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmIndexError); + } + + func test_report_unspecified_error() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorBadRoomCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the max period + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.unspecified); + } + + + + func test_do_not_double_report() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the max period + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); + + // Try to report again the same event + testDelegate.reportedFailure = nil + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + // Pass the grace period + timeShifter.timestamp = TimeInterval(10) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + } + + + func test_ignore_not_member() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + let fakeMembers = FakeRoomMembers() + fakeMembers.mockMembers[myUser] = MXMembership.ban + fakeRoomState.mockMembers = fakeMembers + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the grace period + timeShifter.timestamp = TimeInterval(5) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + } + + + + func test_notification_center() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Shift time below GRACE_PERIOD + timeShifter.timestamp = TimeInterval(2) + + // Simulate event gets decrypted + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + + + // Shift time after GRACE_PERIOD + timeShifter.timestamp = TimeInterval(6) + + + decryptionFailureTracker.checkFailures(); + + // Event should have been graced + XCTAssertNil(testDelegate.reportedFailure); + } + + + func test_should_report_late_decrypt() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Simulate succesful decryption after grace period but before max wait + timeShifter.timestamp = TimeInterval(20) + + // Simulate event gets decrypted + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); + XCTAssertEqual(testDelegate.reportedFailure?.timeToDecrypt, TimeInterval(20)); + + // Assert that it's converted to millis for reporting + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.timeToDecryptMillis, 20000) + + } + + + + func test_should_report_permanent_decryption_error() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Simulate succesful decryption after max wait + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); + XCTAssertNil(testDelegate.reportedFailure?.timeToDecrypt); + + + // Assert that it's converted to -1 for reporting + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.timeToDecryptMillis, -1) + + } +} + diff --git a/RiotTests/FakeUtils.swift b/RiotTests/FakeUtils.swift new file mode 100644 index 0000000000..7bd350e4bc --- /dev/null +++ b/RiotTests/FakeUtils.swift @@ -0,0 +1,109 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + + +class FakeEvent: MXEvent { + + var mockEventId: String; + var mockSender: String!; + var mockDecryptionError: Error? + + init(id: String) { + mockEventId = id + super.init() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override var sender: String! { + get { return mockSender } + set { mockSender = newValue } + } + + override var eventId: String! { + get { return mockEventId } + set { mockEventId = newValue } + } + + override var decryptionError: Error? { + get { return mockDecryptionError } + set { mockDecryptionError = newValue } + } + +} + + +class FakeRoomState: MXRoomState { + + var mockMembers: MXRoomMembers? + + override var members: MXRoomMembers? { + get { return mockMembers } + set { mockMembers = newValue } + } + +} + +class FakeRoomMember: MXRoomMember { + var mockMembership: MXMembership = MXMembership.join + var mockUserId: String! + var mockMembers: MXRoomMembers? = FakeRoomMembers() + + init(mockUserId: String!) { + self.mockUserId = mockUserId + super.init() + } + + override var membership: MXMembership { + get { return mockMembership } + set { mockMembership = newValue } + } + + override var userId: String!{ + get { return mockUserId } + set { mockUserId = newValue } + } + +} + + +class FakeRoomMembers: MXRoomMembers { + + var mockMembers = [String : MXMembership]() + + init(joined: [String] = [String]()) { + for userId in joined { + self.mockMembers[userId] = MXMembership.join + } + super.init() + } + + override func member(withUserId userId: String!) -> MXRoomMember? { + let membership = mockMembers[userId] + if membership != nil { + let mockMember = FakeRoomMember(mockUserId: userId) + mockMember.mockMembership = membership! + return mockMember + } else { + return nil + } + } + +} diff --git a/Tchap/Generated/InfoPlist.swift b/Tchap/Generated/InfoPlist.swift index d65235e8cb..fd4913b7ce 100644 --- a/Tchap/Generated/InfoPlist.swift +++ b/Tchap/Generated/InfoPlist.swift @@ -30,6 +30,8 @@ internal enum InfoPlist { internal static let nsCameraUsageDescription: String = _document["NSCameraUsageDescription"] internal static let nsContactsUsageDescription: String = _document["NSContactsUsageDescription"] internal static let nsFaceIDUsageDescription: String = _document["NSFaceIDUsageDescription"] + internal static let nsLocationAlwaysAndWhenInUseUsageDescription: String = _document["NSLocationAlwaysAndWhenInUseUsageDescription"] + internal static let nsLocationWhenInUseUsageDescription: String = _document["NSLocationWhenInUseUsageDescription"] internal static let nsMicrophoneUsageDescription: String = _document["NSMicrophoneUsageDescription"] internal static let nsPhotoLibraryUsageDescription: String = _document["NSPhotoLibraryUsageDescription"] internal static let uiBackgroundModes: [String] = _document["UIBackgroundModes"] diff --git a/Tchap/Generated/Strings.swift b/Tchap/Generated/Strings.swift index dd83433c6e..9f27d360de 100644 --- a/Tchap/Generated/Strings.swift +++ b/Tchap/Generated/Strings.swift @@ -271,6 +271,10 @@ public class TchapL10n: NSObject { public static var errorTitleDefault: String { return TchapL10n.tr("Tchap", "error_title_default") } + /// Signaler un problème + public static var eventFormatterReportIncident: String { + return TchapL10n.tr("Tchap", "event_formatter_report_incident") + } /// La durée de validité de votre compte a expiré. Un email vous a été envoyé pour la renouveler. Une fois que vous aurez suivi le lien qu’il contient, cliquez ci-dessous. public static var expiredAccountAlertMessage: String { return TchapL10n.tr("Tchap", "expired_account_alert_message") @@ -563,7 +567,7 @@ public class TchapL10n: NSObject { public static var roomCreationTitle: String { return TchapL10n.tr("Tchap", "room_creation_title") } - /// Sinon, consulter cet article de FAQ. + /// En savoir plus. public static var roomDecryptionErrorFaqLinkMessage: String { return TchapL10n.tr("Tchap", "room_decryption_error_faq_link_message") } @@ -743,7 +747,7 @@ public class TchapL10n: NSObject { public static var securityCrossSigningResetMessage: String { return TchapL10n.tr("Tchap", "security_cross_signing_reset_message") } - /// Êtes-vous sur ? + /// Êtes-vous sûr ? public static var securityCrossSigningResetTitle: String { return TchapL10n.tr("Tchap", "security_cross_signing_reset_title") } @@ -791,6 +795,10 @@ public class TchapL10n: NSObject { public static var settingsCryptoImportInvalidFile: String { return TchapL10n.tr("Tchap", "settings_crypto_import_invalid_file") } + /// Sans cette autorisation, les appels entrants ne seront pas notifiés. + public static var settingsEnablePushNotifText: String { + return TchapL10n.tr("Tchap", "settings_enable_push_notif_text") + } /// Les autres utilisateurs ne pourront pas découvrir mon compte lors de leurs recherches public static var settingsHideFromUsersDirectorySummary: String { return TchapL10n.tr("Tchap", "settings_hide_from_users_directory_summary") @@ -855,6 +863,14 @@ public class TchapL10n: NSObject { public static var tchapRoomInvalidLink: String { return TchapL10n.tr("Tchap", "tchap_room_invalid_link") } + /// Vous avez rencontré un souci durant votre appel VoIP. Dites-nous ce qui s'est passé : + public static var voidReportIncidentDescription: String { + return TchapL10n.tr("Tchap", "void_report_incident_description") + } + /// Signaler un problème VoIP + public static var voidReportIncidentTitle: String { + return TchapL10n.tr("Tchap", "void_report_incident_title") + } /// Attention public static var warningTitle: String { return TchapL10n.tr("Tchap", "warning_title") diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 973e2ea63f..79cea6c24b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do before_all do # Ensure used Xcode version - xcversion(version: "14.2") + xcversion(version: "15.2") end #### Public #### @@ -142,7 +142,7 @@ platform :ios do run_tests( workspace: "Tchap.xcworkspace", scheme: "RiotSwiftUITests", - device: "iPhone 14", + device: "iPhone 15", code_coverage: true, # Test result configuration result_bundle: true, diff --git a/project.yml b/project.yml index 5beeba117f..60a1bfb830 100644 --- a/project.yml +++ b/project.yml @@ -52,7 +52,7 @@ include: packages: AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events - exactVersion: 0.5.0 + exactVersion: 0.15.0 Mapbox: url: https://github.com/maplibre/maplibre-gl-native-distribution minVersion: 5.12.2