diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 424d5e004b..a305eca352 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -426,10 +426,15 @@ final class RiotSettings: NSObject { var tchapSecureBackupNextDisplayDate // Tchap has tried to automatically activate cross-signing. - // This automatic activation must be tried only once because if it fails, it could pro;pt user each time the application is activated. + // This automatic activation must be tried only once because if it fails, it could prompt user each time the application is activated. @UserDefault(key: "tchapCrossSigningAutoActivationTried", defaultValue: false, storage: defaults) var tchapCrossSigningAutoActivationTried + // Tchap migrate to new Tchap last display. + // Register the last time the new Tchap Migration alert was displayed to decide if enough time has spent before showing the alert again. + @UserDefault(key: "tchapNewTchapMigrationAlertLastPresentationDate", defaultValue: Date.distantPast, storage: defaults) + var tchapNewTchapMigrationAlertLastPresentationDate + } // MARK: - RiotSettings notification constants diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index ef8784c4f7..08a1f396aa 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -272,6 +272,50 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { self.createLeftButtonItem(for: allChatsViewController) } + // Tchap: check if Migration to new Tchap View should be presented to user. + private func tchapCheckMigration(userSession: UserSession) { + // Tchap: check for new Tchap advertizing in wellknown. + Task { + + // Don't present alert more than once a day. + guard Date.now.timeIntervalSince(RiotSettings.shared.tchapNewTchapMigrationAlertLastPresentationDate) > 86400 else { + return + } + + RiotSettings.shared.tchapNewTchapMigrationAlertLastPresentationDate = .now + + if let homeServerName = MXTools.serverName(inMatrixIdentifier: userSession.userId), + let newTchapAppStoreUrl = await newTchapAppStoreUrl(for: homeServerName) { + + // Check if a ViewController is already presented. If so, present our Migration view from this presented view controller. + Task { @MainActor in + + let presentingViewController = (navigationRouter as? NavigationRouter)?.presentedViewController() + + // Prepare Coordinator + let checkMigrateToNewTchapCoordinator = MigrateToNewTchapCoordinator(appStoreUrl: newTchapAppStoreUrl) { [weak self] in + // Dismiss Migration to new Tchap View from right context. + if let presentingViewController { + presentingViewController.dismiss(animated: true, completion: nil) + } else { + self?.navigationRouter.dismissModule(animated: true, completion: nil) + } + } + + // Start Coordinator + checkMigrateToNewTchapCoordinator.start() + + // Present Coordinator in right context. + if let presentingViewController { + presentingViewController.present(checkMigrateToNewTchapCoordinator.toPresentable(), animated: true) + } else { + navigationRouter.present(checkMigrateToNewTchapCoordinator, animated: true) + } + } + } + } + } + @objc private func userSessionsServiceWillRemoveUserSession(_ notification: Notification) { guard let userSession = notification.userInfo?[UserSessionsService.NotificationUserInfoKey.userSession] as? UserSession else { return @@ -702,6 +746,33 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { viewController.loadViewIfNeeded() return viewController } + + + // Tchap: check advertizing of new Tchap in returned wellknown + // + // Tchap: the key "fr.gouv.tchap.tchapx" is present in the payload if the homeserver advertizes the new Tchap. + // Check matrix/client wellknown content for "fr.gouv.tchap.tchapx" key. + // + // The awaited content is the urls of the applications on the application stores. + // + // "fr.gouv.tchap.tchapx": { + // "android": "https://store.tchapx.android", + // "ios": "https://apps.apple.com/fr/app/tchap-x/id6736991029" + // } + // + // Returns the new Tchap appStore URL if found in well-known. + private func newTchapAppStoreUrl(for homeserverName: String) async -> URL? { + let homeserverAddress = HomeserverAddress.sanitized(homeserverName) + guard let homeserverURL = URL(string: homeserverAddress), + let wellknown = try? await AuthenticationService.shared.wellKnown(for: homeserverURL), + let wellknownEntries = wellknown.jsonDictionary() as? [String: Any], + let newTchapEntry = wellknownEntries["fr.gouv.tchap.tchapx"] as? [String: String], + let newTchapIosEntry = newTchapEntry["ios"], + let appStoreUrl = URL(string: newTchapIosEntry) else { + return nil + } + return appStoreUrl + } } extension AllChatsCoordinator: SignOutFlowPresenterDelegate { @@ -766,6 +837,16 @@ extension AllChatsCoordinator: AllChatsViewControllerDelegate { self.navigationRouter.present(router, animated: true) } + + // Tchap: new Tchap migration + func allChatsViewControllerShouldShowNewTchapMigration(_ allChatsViewController: AllChatsViewController) { + // This method can be called by `viewDidAppear` before device is verified. + // In this case, userSession will be nil. + guard let userSession = self.parameters.userSessionsService.mainUserSession else { + return + } + tchapCheckMigration(userSession: userSession) + } } // MARK: - RoomCoordinatorDelegate diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 4cba34df28..4f644a35c2 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -18,6 +18,7 @@ protocol AllChatsViewControllerDelegate: AnyObject { // Tchap: Add methods to use Tchap specific flows. func allChatsViewControllerShouldOpenRoomCreation(_ allChatsViewController: AllChatsViewController) func allChatsViewControllerShouldOpenRoomList(_ allChatsViewController: AllChatsViewController) + func allChatsViewControllerShouldShowNewTchapMigration(_ allChatsViewController: AllChatsViewController) } class AllChatsViewController: HomeViewController { @@ -178,7 +179,10 @@ class AllChatsViewController: HomeViewController { return } - AppDelegate.theDelegate().checkAppVersion() + // Tchap: Tchap doesn't check for version update anymore. +// AppDelegate.theDelegate().checkAppVersion() + + allChatsDelegate?.allChatsViewControllerShouldShowNewTchapMigration(self) if BuildSettings.newAppLayoutEnabled && !RiotSettings.shared.allChatsOnboardingHasBeenDisplayed { // Tchap: Disable new layout tutorials. Wait for an updated tutorials before show it. diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 8ecc707f7e..a3bbb83c1e 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -403,3 +403,10 @@ extension NavigationRouter { static let navigationController = "navigationController" } } + +// Tchap: Used to know from which controller or coordinator to present New Tchap Migration View. +extension NavigationRouter { + func presentedViewController() -> UIViewController? { + navigationController.presentedViewController + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift index 496f250eb7..2e19d3d561 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -320,7 +320,9 @@ class AuthenticationService: NSObject { } /// Perform a well-known request on the specified homeserver URL. - private func wellKnown(for homeserverURL: URL) async throws -> MXWellKnown { + // Tchap: make wellknown() method available outside (needed in AllChatsCoordinator to check the existence of new Tchap application). + // private func wellKnown(for homeserverURL: URL) async throws -> MXWellKnown { + func wellKnown(for homeserverURL: URL) async throws -> MXWellKnown { let wellKnownClient = clientType.init(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) // The .well-known/matrix/client API is often just a static file returned with no content type. diff --git a/Tchap/Assets/Localizations/fr.lproj/Tchap.strings b/Tchap/Assets/Localizations/fr.lproj/Tchap.strings index 3b0273229d..3037362749 100644 --- a/Tchap/Assets/Localizations/fr.lproj/Tchap.strings +++ b/Tchap/Assets/Localizations/fr.lproj/Tchap.strings @@ -379,3 +379,10 @@ // Chat participants "room_participants_leave_not_allowed_for_last_owner_msg" = "Vous ne pouvez pas quitter ce salon car vous êtes le seul administrateur."; +//////////////////////////////////////////////////////////////////////////////// +// MARK: Migrate to New Tchap +"migrate_to_new_tchap_title" = "Tchap évolue !"; +"migrate_to_new_tchap_message" = "Une nouvelle application est disponible pour iPhone. Veuillez la télécharger pour accéder à de nouvelles fonctionnalités."; +"migrate_to_new_tchap_warning" = "L'ancienne application sera bientôt abandonnée. Pour migrer, installez la nouvelle application, connectez-vous en vérifiant votre identité, puis supprimez l'ancienne."; +"migrate_to_new_tchap_action_accept_title" = "Télécharger"; +"migrate_to_new_tchap_action_help_title" = "Aide à la migration"; diff --git a/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/Contents.json b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/Contents.json new file mode 100644 index 0000000000..174c29427c --- /dev/null +++ b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "next_tchap_logo@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "next_tchap_logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "next_tchap_logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@1x.png b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@1x.png new file mode 100644 index 0000000000..9f045ac8ba Binary files /dev/null and b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@1x.png differ diff --git a/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@2x.png b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@2x.png new file mode 100644 index 0000000000..72e569c987 Binary files /dev/null and b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@2x.png differ diff --git a/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@3x.png b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@3x.png new file mode 100644 index 0000000000..4bad19b26b Binary files /dev/null and b/Tchap/Assets/SharedImages.xcassets/NextTchapLogo.imageset/next_tchap_logo@3x.png differ diff --git a/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinator.swift b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinator.swift new file mode 100644 index 0000000000..889b75eb32 --- /dev/null +++ b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinator.swift @@ -0,0 +1,57 @@ +/* + Copyright 2019 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 UIKit + +final class MigrateToNewTchapCoordinator: MigrateToNewTchapCoordinatorType { + + // MARK: Constant + + private enum Constants { + static let nextTchapHelpURL = "https://app.crisp.chat/website/6dacc68e-de3a-4511-8177-1339616098de/helpdesk/articles/fr/45a1de71-1093-4392-ab09-c88a6338c181/" + } + + // MARK: - Properties + + // MARK: Private + + private(set) var migrateToNewTchapViewController: MigrateToNewTchapViewController! + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + weak var delegate: MigrateToNewTchapCoordinatorDelegate? + + // MARK: - Setup + + init(appStoreUrl: URL, cancelAction: @escaping () -> Void) { + let migrateToNewTchapViewModel = MigrateToNewTchapViewModel( + appStoreAppUrl: appStoreUrl, + helpArticleUrl: URL(string: Constants.nextTchapHelpURL)!, // swiftlint:disable:this force_unwrapping + actionCancel: cancelAction) + migrateToNewTchapViewController = MigrateToNewTchapViewController.instantiate(with: migrateToNewTchapViewModel) + } + + // MARK: - Public methods + + func start() { + } + + func toPresentable() -> UIViewController { + return self.migrateToNewTchapViewController + } +} diff --git a/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinatorType.swift b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinatorType.swift new file mode 100644 index 0000000000..de3d3d5788 --- /dev/null +++ b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapCoordinatorType.swift @@ -0,0 +1,25 @@ +/* + Copyright 2019 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 MigrateToNewTchapCoordinatorDelegate: AnyObject { + func migrateToNewTchapCoordinatorDidCancel(_ coordinator: MigrateToNewTchapCoordinatorType) +} + +protocol MigrateToNewTchapCoordinatorType: Coordinator, Presentable { + var delegate: MigrateToNewTchapCoordinatorDelegate? { get set } +} diff --git a/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapView.swift b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapView.swift new file mode 100644 index 0000000000..5b23302d05 --- /dev/null +++ b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapView.swift @@ -0,0 +1,145 @@ +// +// Copyright 2026 New Vector Ltd +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import SwiftUI + +struct MigrateToNewTchapView: View { + @Environment(\.openURL) private var openURL + + @ObservedObject var viewModel: MigrateToNewTchapViewModel + + private func openAppStoreAction() { + openURL(viewModel.appStoreAppUrl) + } + + private func presentHelpAction() { + viewModel.shouldPresentHelp = true + } + + var iconsHeader: some View { + HStack(spacing: 0.0) { + Image(uiImage: Asset.SharedImages.tchapLogo.image) + .resizable() + .scaledToFit() + .padding(.vertical, 8.0) + Image(systemName: "arrow.right") + .font(.title) + .foregroundStyle(Color(uiColor: viewModel.theme.textSecondaryColor)) + .padding(.horizontal, 16.0) + Image(uiImage: Asset_tchap.SharedImages.nextTchapLogo.image) + .resizable() + .scaledToFit() + } + } + + var title: some View { + Text(TchapL10n.migrateToNewTchapTitle) + .font(.title2) + .bold() + .foregroundStyle(Color(uiColor: viewModel.theme.textPrimaryColor)) + } + + var message: some View { + Text(TchapL10n.migrateToNewTchapMessage) + .font(.callout) + .multilineTextAlignment(.center) + .foregroundStyle(Color(uiColor: viewModel.theme.textSecondaryColor)) + } + + var warning: some View { + HStack(alignment: .top, spacing: 12.0) { + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(Color(uiColor: viewModel.theme.warningColor)) + Text(TchapL10n.migrateToNewTchapWarning) + .font(.callout) + .foregroundStyle(Color(uiColor: viewModel.theme.textPrimaryColor)) + } + .padding() + .background( + RoundedRectangle(cornerRadius: 8.0) + .fill(Color(uiColor: UIColor(rgb: viewModel.isCurrentThemeDark ? 0x440505 : 0xFFEFED))) + ) + } + + var appStoreButton: some View { + Button { + openAppStoreAction() + } label: { + Text(TchapL10n.migrateToNewTchapActionAcceptTitle) + .font(.callout) + .bold() + .padding() + .frame(maxWidth: .infinity) + .background(Color(uiColor: viewModel.theme.tintColor)) + .cornerRadius(4.0) + .foregroundColor(.white) + } + } + + var helpButton: some View { + Button { + presentHelpAction() + } label: { + Text(TchapL10n.migrateToNewTchapActionHelpTitle) + .font(.callout) + .bold() + .padding() + .frame(maxWidth: .infinity) + .foregroundColor(Color(uiColor: viewModel.theme.textSecondaryColor)) + } + } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem { + Button { + viewModel.actionCancel() + } label: { + Text(VectorL10n.skip) + } + } + } + + var body: some View { + NavigationView { + VStack { + iconsHeader + .frame(height: 96.0) + .padding(.bottom, 32.0) + + title + .padding(.bottom, 8.0) + + message + .padding(.bottom, 16.0) + + warning + .padding(.bottom, 32.0) + + appStoreButton + + helpButton + + } + .padding(32.0) + .background(Color(uiColor:.systemBackground)) + + .toolbar { + toolbar + } + } + .sheet(isPresented: $viewModel.shouldPresentHelp) { + WebSheetView(targetUrl: viewModel.helpArticleUrl) + } + } +} + +#Preview { + MigrateToNewTchapView(viewModel: MigrateToNewTchapViewModel(appStoreAppUrl: URL(string: "https://apple.com")!, // swiftlint:disable:this force_unwrapping + helpArticleUrl: URL(string: "https://www.tchap.gouv.fr")!, // swiftlint:disable:this force_unwrapping + actionCancel: {})) +} diff --git a/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewController.swift b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewController.swift new file mode 100644 index 0000000000..08eb9e7a41 --- /dev/null +++ b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewController.swift @@ -0,0 +1,77 @@ +/* + Copyright 2019 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 UIKit +import SwiftUI + +@preconcurrency final class MigrateToNewTchapViewController: UIHostingController { + + // MARK: - Properties + + // MARK: Private + + private var viewModel: MigrateToNewTchapViewModel! + + // MARK: Public + + // MARK: - Setup + + static func instantiate(with viewModel: MigrateToNewTchapViewModel) -> MigrateToNewTchapViewController { + let view = MigrateToNewTchapView(viewModel: viewModel) + let viewController = MigrateToNewTchapViewController(rootView: view) + viewController.viewModel = viewModel + return viewController + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + registerThemeServiceDidChangeThemeNotification() + + // Do any additional setup after loading the view. + + self.setupViews() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.themeDidChange() + } + + // MARK: - Private + + private func updateTheme() { + self.viewModel.theme = ThemeService.shared().theme + } + + @objc private func themeDidChange() { + self.updateTheme() + } + + private func setupViews() { + } + + func presentHelp() { + viewModel.shouldPresentHelp = true + } +} diff --git a/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewModel.swift b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewModel.swift new file mode 100644 index 0000000000..c7912038cb --- /dev/null +++ b/Tchap/Modules/Application/MigrateToNewTchap/MigrateToNewTchapViewModel.swift @@ -0,0 +1,36 @@ +/* + Copyright 2019 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 + +final class MigrateToNewTchapViewModel: ObservableObject { + @Published var theme: Theme + var isCurrentThemeDark: Bool { ThemeService.shared().isCurrentThemeDark() } + @Published var shouldPresentHelp = false + let appStoreAppUrl: URL + let helpArticleUrl: URL + + let actionCancel: () -> Void + + init(appStoreAppUrl: URL, + helpArticleUrl: URL, + actionCancel: @escaping () -> Void) { + theme = ThemeService.shared().theme + self.appStoreAppUrl = appStoreAppUrl + self.helpArticleUrl = helpArticleUrl + self.actionCancel = actionCancel + } +} diff --git a/Tchap/target.yml b/Tchap/target.yml index 0d1da67ad9..1c899a5304 100644 --- a/Tchap/target.yml +++ b/Tchap/target.yml @@ -117,6 +117,7 @@ targetTemplates: - path: ../Tchap/Generated/Strings.swift - path: ../Tchap/Modules/Application/VersionUpdate - path: ../Tchap/Modules/Room/Views/BubbleCells/Antivirus + - path: ../Tchap/Modules/Application/MigrateToNewTchap # Add separately localizable files