diff --git a/WireAPI/Package.swift b/WireAPI/Package.swift index fca52819371..ac5f763e78c 100644 --- a/WireAPI/Package.swift +++ b/WireAPI/Package.swift @@ -37,6 +37,7 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing") ], resources: [ + .process("APIs/AccountsAPI/Resources"), .process("APIs/BackendInfoAPI/Resources"), .process("APIs/ConnectionsAPI/Resources"), .process("APIs/ConversationsAPI/Resources"), diff --git a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift index 502bb71674c..43268c8eb85 100644 --- a/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift +++ b/WireAPI/Sources/WireAPI/APIs/AccountsAPI/AccountsAPIV7.swift @@ -51,7 +51,10 @@ class AccountsAPIV7: AccountsAPIV6 { requiringAccessToken: true ) - return try ResponseParser() + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + return try ResponseParser(decoder: decoder) .success(code: .ok, type: UpgradeToTeamResponseV7.self) .failure(code: .forbidden, label: "user-already-in-a-team", error: AccountsAPIError.userAlreadyInATeam) .failure(code: .notFound, label: "not-found", error: AccountsAPIError.userNotFound) diff --git a/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/AccountsAPITests.swift b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/AccountsAPITests.swift new file mode 100644 index 00000000000..ed3e947c919 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/AccountsAPITests.swift @@ -0,0 +1,124 @@ +// +// Wire +// Copyright (C) 2024 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import XCTest + +@testable import WireAPI +@testable import WireAPISupport + +final class AccountsAPITests: XCTestCase { + + private var apiSnapshotHelper: APIServiceSnapshotHelper! + + // MARK: - Setup + + override func setUp() { + apiSnapshotHelper = APIServiceSnapshotHelper { apiService, apiVersion in + AccountsAPIBuilder(apiService: apiService) + .makeAPI(for: apiVersion) + + } + } + + override func tearDown() { + apiSnapshotHelper = nil + } + + // MARK: - Request generation + + func testUpgradeToTeam_V0_To_V6() async throws { + // Given + let apiService = MockAPIServiceProtocol() + let builder = AccountsAPIBuilder(apiService: apiService) + + for apiVersion in [APIVersion.v0, .v1, .v2, .v3, .v4, .v5, .v6] { + let sut = builder.makeAPI(for: apiVersion) + + // Then + await XCTAssertThrowsErrorAsync(AccountsAPIError.unsupportedEndpointForAPIVersion) { + try await sut.upgradeToTeam(teamName: Scaffolding.teamName) + } + } + } + + func testUpgradeToTeam_Request_Generation_V7_Onwards() async throws { + // Given + let apiVersions = APIVersion.v7.andNextVersions + + // Then + try await apiSnapshotHelper.verifyRequest(for: apiVersions) { sut in + // When + _ = try await sut.upgradeToTeam(teamName: Scaffolding.teamName) + } + } + + // MARK: - Response handling + + func testUpgradeToTeam_Response_Handling_V7_Success() async throws { + // Given + let apiService = MockAPIServiceProtocol.withResponses([ + (.ok, "UpgradeToTeamSuccessResponse") + ]) + + let sut = AccountsAPIV7(apiService: apiService) + + // When + let response = try await sut.upgradeToTeam(teamName: Scaffolding.teamName) + + // Then + XCTAssertEqual(response, UpgradedAccountTeam(teamId: Scaffolding.teamID, teamName: Scaffolding.teamName)) + } + + func testUpgradeToTeam_Response_Handling_V7_User_Already_In_A_Team() async throws { + // Given + let apiService = MockAPIServiceProtocol.withResponses([ + (.forbidden, "UpgradeToTeamErrorResponse_UserAlreadyInATeam") + ]) + + let sut = AccountsAPIV7(apiService: apiService) + + // Then + await XCTAssertThrowsErrorAsync(AccountsAPIError.userAlreadyInATeam) { + // When + try await sut.upgradeToTeam(teamName: Scaffolding.teamName) + } + } + + func testUpgradeToTeam_Response_Handling_V7_User_Not_Found() async throws { + // Given + let apiService = MockAPIServiceProtocol.withResponses([ + (.notFound, "UpgradeToTeamErrorResponse_UserNotFound") + ]) + + let sut = AccountsAPIV7(apiService: apiService) + + // Then + await XCTAssertThrowsErrorAsync(AccountsAPIError.userNotFound) { + // When + try await sut.upgradeToTeam(teamName: Scaffolding.teamName) + } + } + +} + +private enum Scaffolding { + + static let teamName = "iOS Team" + static let teamID = UUID(uuidString: "66dc3593-4c3a-49e8-b5c3-d3d908bd7403")! + +} diff --git a/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserAlreadyInATeam.json b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserAlreadyInATeam.json new file mode 100644 index 00000000000..d40896d14ef --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserAlreadyInATeam.json @@ -0,0 +1,5 @@ +{ + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" +} diff --git a/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserNotFound.json b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserNotFound.json new file mode 100644 index 00000000000..9007c394c01 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamErrorResponse_UserNotFound.json @@ -0,0 +1,5 @@ +{ + "code": 404, + "label": "not-found", + "message": "User not found" +} diff --git a/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamSuccessResponse.json b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamSuccessResponse.json new file mode 100644 index 00000000000..1156838a8a2 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/Resources/UpgradeToTeamSuccessResponse.json @@ -0,0 +1,4 @@ +{ + "team_id": "66dc3593-4c3a-49e8-b5c3-d3d908bd7403", + "team_name": "iOS Team" +} diff --git a/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/__Snapshots__/AccountsAPITests/testUpgradeToTeam_Request_Generation_V7_Onwards.request-0-v7.txt b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/__Snapshots__/AccountsAPITests/testUpgradeToTeam_Request_Generation_V7_Onwards.request-0-v7.txt new file mode 100644 index 00000000000..06358db4202 --- /dev/null +++ b/WireAPI/Tests/WireAPITests/APIs/AccountsAPI/__Snapshots__/AccountsAPITests/testUpgradeToTeam_Request_Generation_V7_Onwards.request-0-v7.txt @@ -0,0 +1,5 @@ +curl \ + --request POST \ + --header "Content-Type: application/json" \ + --data "{\"name\":\"iOS Team\",\"icon\":\"default\"}" \ + "upgrade-personal-to-team" \ No newline at end of file diff --git a/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift b/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift index 0d4cc1aab90..afcc9c2fc83 100644 --- a/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift +++ b/WireDomain/Sources/WireDomain/UseCases/IndividualToTeamMigrationUseCaseImplementation.swift @@ -18,26 +18,35 @@ import Foundation @preconcurrency import WireAPI +@preconcurrency import WireDataModel import WireDomainAPI import WireLogging import WireSystem public struct IndividualToTeamMigrationUseCaseImplementation: IndividualToTeamMigrationUseCase { + private let accountsAPI: AccountsAPI + private let context: NSManagedObjectContext private let logger: WireLogger = .individualToTeamMigration - public init(apiService: APIServiceProtocol, apiVersion: APIVersion) { - self.accountsAPI = AccountsAPIBuilder(apiService: apiService).makeAPI(for: apiVersion) + public init( + accountsAPI: AccountsAPI, + context: NSManagedObjectContext + ) { + self.accountsAPI = accountsAPI + self.context = context } public func invoke(teamName: String) async throws -> IndividualToTeamMigrationResult { logger.debug("Migrating individual account to team account") do { let upgradeResult = try await accountsAPI.upgradeToTeam(teamName: teamName) - logger.debug("Individual account migrated to team account") + logger.debug("Individual account migrated to team account, storing team locally...") + try await createTeamLocally(id: upgradeResult.teamId, name: upgradeResult.teamName) + logger.debug("Individual to team migration completed successfully") return IndividualToTeamMigrationResult(teamID: upgradeResult.teamId, teamName: upgradeResult.teamName) } catch { - logger.error("Failed to migrate individual account to team account") + logger.error("Failed to migrate individual account to team account: \(error.localizedDescription)") switch error { case AccountsAPIError.userAlreadyInATeam: throw IndividualToTeamMigrationError.userAlreadyInTeam @@ -46,4 +55,34 @@ public struct IndividualToTeamMigrationUseCaseImplementation: IndividualToTeamMi } } } + + private func createTeamLocally( + id: UUID, + name: String + ) async throws { + try await context.perform { [context] in + let team = Team.fetchOrCreate( + with: id, + in: context + ) + + team.name = name + team.needsToBeUpdatedFromBackend = true + + // Probably there's nothing to fetch since it's a brand + // new team, but just in case. + team.needsToDownloadRoles = true + team.needsToRedownloadMembers = true + + let selfUser = ZMUser.selfUser(in: context) + + _ = Member.getOrUpdateMember( + for: selfUser, + in: team, + context: context + ) + + try context.save() + } + } } diff --git a/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift index 3e381e8194d..5cb419e764a 100644 --- a/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift +++ b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift @@ -76,8 +76,8 @@ public enum KeychainQueryItem: Hashable, Equatable, Sendable { case let .account(string): (kSecAttrAccount, string) - case let .itemClass(itemClass): - (kSecClass, itemClass) + case .itemClass(.genericPassword): + (kSecClass, kSecClassGenericPassword) case .accessible(.afterFirstUnlock): (kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock) diff --git a/WireUI/Sources/WireDesign/Colors/ColorTheme.swift b/WireUI/Sources/WireDesign/Colors/ColorTheme.swift index 0fb46f1c876..48e9cf8653a 100644 --- a/WireUI/Sources/WireDesign/Colors/ColorTheme.swift +++ b/WireUI/Sources/WireDesign/Colors/ColorTheme.swift @@ -44,6 +44,8 @@ public enum ColorTheme { public static let onHighlight = UIColor(light: .black, dark: .black) public static let secondaryText = UIColor(light: .gray70, dark: .gray60) + + public static let requiredField = UIColor(light: .red500Light, dark: .red500Dark) } public enum Backgrounds { @@ -124,8 +126,8 @@ public enum ColorTheme { } public enum Checkbox { - public static let enabled = UIColor(light: .blue500Light, dark: .blue500Dark) - public static let disabled = UIColor(light: .gray50, dark: .gray80) + public static let enabled = UIColor(light: .gray50, dark: .gray80) + public static let selected = UIColor(light: .blue500Light, dark: .blue500Dark) } public enum Strokes { diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/Checkbox.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/Checkbox.swift index cd62b146dd9..cdf16a79ef5 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/Checkbox.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/Checkbox.swift @@ -41,12 +41,9 @@ struct Checkbox: View { }, label: { Image(systemName: isChecked ? "checkmark.square.fill" : "square") .font(.system(size: 24)) - .backgroundStyle(Color( - uiColor: isChecked ? ColorTheme.Checkbox.enabled : ColorTheme.Checkbox - .disabled - )) }) .buttonStyle(.plain) + .foregroundStyle(isChecked ? ColorTheme.Checkbox.selected.color : ColorTheme.Checkbox.enabled.color) Text(title) .wireTextStyle(.subline1) } diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/PageContainer.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/PageContainer.swift index 066f5bfa072..393518dae2e 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/PageContainer.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Components/PageContainer.swift @@ -40,20 +40,25 @@ struct PageContainer: View { } var body: some View { - ScrollView { - VStack { - Text(String.formated(key: "individualToTeam.progressCount", bundle: .module, step, stepCount)) - .wireTextStyle(.subline1) - .foregroundStyle(Color(uiColor: ColorTheme.Base.secondaryText)) - Spacer() - .frame(height: 12) - Text(stepTitle) - .wireTextStyle(.h2) - Spacer(minLength: 36) - content + GeometryReader { proxy in + ScrollView { + VStack { + Text(String.formated(key: "individualToTeam.progressCount", bundle: .module, step, stepCount)) + .wireTextStyle(.subline1) + .foregroundStyle(Color(uiColor: ColorTheme.Base.secondaryText)) + Spacer() + .frame(height: 12) + Text(stepTitle) + .wireTextStyle(.h2) + Spacer(minLength: 36) + content + } + .frame( + minHeight: proxy.size.height - 24 + ) } + .padding(.horizontal, 16) + .padding(.bottom, 24) } - .padding(.horizontal, 16) - .padding(.bottom, 24) } } diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/IndividualToTeamMigrationViewController.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/IndividualToTeamMigrationViewController.swift index efbb31f15a4..fe2fa465b70 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/IndividualToTeamMigrationViewController.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/IndividualToTeamMigrationViewController.swift @@ -26,6 +26,7 @@ import WireReusableUIComponents public class IndividualToTeamMigrationViewController: UIViewController { public enum Action: Sendable { case cancel + case toLearnMoreAboutPlans case completionGoToApp case completionGoToTeamManagement } @@ -33,7 +34,7 @@ public class IndividualToTeamMigrationViewController: UIViewController { enum Step: Sendable { case teamPlanSelection(features: [TeamPlanFeature]) case teamName - case confirmation(teamName: String) + case confirmation(teamName: String, termsOfUseURL: String, privacyPolicyURL: String) case completion(profileName: String, teamName: String) var title: String { @@ -86,8 +87,8 @@ public class IndividualToTeamMigrationViewController: UIViewController { enum Transition: Sendable { case toCancellationAlert - case dismissCancellationAlert case toPlans + case toLearnMoreAboutPlans case toTeamName case toConfirmation(teamName: String) case toTeamCreation(teamName: String) @@ -105,11 +106,15 @@ public class IndividualToTeamMigrationViewController: UIViewController { let childController: UINavigationController var currentStep: Step let features: [TeamPlanFeature] + let termsOfUseURL: String + let privacyPolicyURL: String let useCase: any IndividualToTeamMigrationUseCase let userProfileName: String public init( features: [TeamPlanFeature], + privacyPolicyURL: String, + termsOfUseURL: String, useCase: any IndividualToTeamMigrationUseCase, userProfileName: String, actionCallback: @escaping @Sendable (Action) -> Void @@ -118,18 +123,24 @@ public class IndividualToTeamMigrationViewController: UIViewController { self.currentStep = .teamPlanSelection(features: features) self.childController = UINavigationController() self.features = features + self.privacyPolicyURL = privacyPolicyURL + self.termsOfUseURL = termsOfUseURL self.useCase = useCase self.userProfileName = userProfileName super.init(nibName: nil, bundle: nil) } public convenience init( + privacyPolicyURL: String, + termsOfUseURL: String, useCase: any IndividualToTeamMigrationUseCase, userProfileName: String, actionCallback: @escaping @Sendable (Action) -> Void ) { self.init( features: .features, + privacyPolicyURL: privacyPolicyURL, + termsOfUseURL: termsOfUseURL, useCase: useCase, userProfileName: userProfileName, actionCallback: actionCallback @@ -146,7 +157,7 @@ public class IndividualToTeamMigrationViewController: UIViewController { addChild(childController) view.addSubview(childController.view) childController.didMove(toParent: self) - childController.navigationBar.tintColor = .darkText + childController.navigationBar.tintColor = ColorTheme.Backgrounds.onBackground transition(to: .toPlans) } @@ -157,13 +168,9 @@ public class IndividualToTeamMigrationViewController: UIViewController { let alert = cancellationSheetFactory( onLeave: { [weak self] in self?.actionCallback(.cancel) - }, onContinue: { [weak self] in - self?.transition(to: .dismissCancellationAlert) - } + }, onContinue: {} ) childController.present(alert, animated: true) - case .dismissCancellationAlert: - childController.dismiss(animated: true) case .toPlans: let vc = hostedView( for: .teamPlanSelection(features: features), @@ -172,6 +179,8 @@ public class IndividualToTeamMigrationViewController: UIViewController { onTransition: { @MainActor [weak self] in self?.transition(to: $0) } ) childController.pushViewController(vc, animated: false) + case .toLearnMoreAboutPlans: + actionCallback(.toLearnMoreAboutPlans) case .toTeamName: let vc = hostedView( for: .teamName, @@ -182,16 +191,22 @@ public class IndividualToTeamMigrationViewController: UIViewController { childController.pushViewController(vc, animated: true) case let .toConfirmation(teamName): let vc = hostedView( - for: .confirmation(teamName: teamName), - stepIndex: childController.viewControllers.count + 1, + for: .confirmation( + teamName: teamName, + termsOfUseURL: termsOfUseURL, + privacyPolicyURL: privacyPolicyURL + ), + stepIndex: 4, stepCount: 4, onTransition: { @MainActor [weak self] in self?.transition(to: $0) } ) childController.pushViewController(vc, animated: true) case let .toTeamCreation(teamName: teamName): createTeam(named: teamName) - case let .toError(error): + case let .toError(error as IndividualToTeamMigrationError): displayError(error) + case let .toError(error): + displayGenericError(error) case let .toCompletion(teamName): let vc = hostedView( for: .completion(profileName: userProfileName, teamName: teamName), @@ -199,7 +214,7 @@ public class IndividualToTeamMigrationViewController: UIViewController { stepCount: 4, onTransition: { @MainActor [weak self] in self?.transition(to: $0) } ) - childController.pushViewController(vc, animated: true) + childController.setViewControllers([vc], animated: true) case .toApp: actionCallback(.completionGoToApp) case .toTeamManagement: @@ -230,11 +245,11 @@ public class IndividualToTeamMigrationViewController: UIViewController { action: .localized(key: "individualToTeam.error.alreadyPartOfTeam.action", bundle: .module) ) case let .generic(error): - displayError(error) + displayGenericError(error) } } - private func displayError(_ error: some Error) { + private func displayGenericError(_ error: some Error) { displayError( title: .localized(key: "individualToTeam.error.generic.title", bundle: .module), body: .localized(key: "individualToTeam.error.generic.body", bundle: .module), @@ -271,6 +286,7 @@ private func hostedView( stepCount: stepCount, stepTitle: step.title ) + .environment(\.wireTextStyleMapping, WireTextStyleMapping()) .ignoresSafeArea(.container, edges: .bottom) ) vc.title = step.title @@ -283,6 +299,7 @@ private func hostedView( // Hide navigation bar title vc.navigationItem.titleView = UIView() vc.navigationItem.rightBarButtonItem?.tintColor = ColorTheme.Backgrounds.onBackground + vc.navigationItem.leftBarButtonItem?.tintColor = ColorTheme.Backgrounds.onBackground return vc } @@ -302,7 +319,7 @@ private func viewFor( TeamPlanSelectionView(features: features) { action in switch action { case .goToPlans: - transitionCallback(.toPlans) + transitionCallback(.toLearnMoreAboutPlans) case .continue: transitionCallback(.toTeamName) } @@ -314,8 +331,11 @@ private func viewFor( transitionCallback(.toConfirmation(teamName: teamName)) } } - case let .confirmation(teamName): - ConfirmationView { action in + case let .confirmation(teamName, termsOfUseURL, privacyPolicyURL): + ConfirmationView( + termsOfUseURL: termsOfUseURL, + privacyPolicyURL: privacyPolicyURL + ) { action in switch action { case .continue: transitionCallback(.toTeamCreation(teamName: teamName)) diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Resources/Localizable.xcstrings b/WireUI/Sources/WireIndividualToTeamMigrationUI/Resources/Localizable.xcstrings index 5428d58e2ba..efd271cc5f5 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Resources/Localizable.xcstrings +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Resources/Localizable.xcstrings @@ -162,13 +162,32 @@ } } }, - "individualToTeam.confirmation.body" : { - "extractionState" : "manual", + "individualToTeam.confirmation.body.createTeam" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You create a team and transfer your personal account into a team account" + } + } + } + }, + "individualToTeam.confirmation.body.permanent" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This change is permanent and irrevocable" + } + } + } + }, + "individualToTeam.confirmation.body.teamOwner" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "• You create a team and transfer your personal account into a team account\n• As the team owner you can invite and remove team members and manage team settings\n• This change is permanent and irrevocable" + "value" : "As the team owner you can invite and remove team members and manage team settings" } } } @@ -222,7 +241,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Accept our [Terms & Conditions](https://wire.com/en/terms-of-use-business) and [Privacy Policy](https://wire.com/en/privacy-policy)." + "value" : "Accept our [Terms & Conditions](%@) and [Privacy Policy](%@)." } } } @@ -412,17 +431,6 @@ } } }, - "individualToTeam.plansURL" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "https://wire.com/en/pricing" - } - } - } - }, "individualToTeam.progressCount" : { "extractionState" : "manual", "localizations" : { @@ -461,7 +469,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your team" + "value" : "Your Team" } } } @@ -472,7 +480,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Team name *" + "value" : "Team name" } } } @@ -490,4 +498,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/ConfirmationView.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/ConfirmationView.swift index 6cf8ddd670b..a6368e2f4c6 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/ConfirmationView.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/ConfirmationView.swift @@ -26,14 +26,29 @@ struct ConfirmationView: View { case `continue` } + let termsOfUseURL: String + let privacyPolicyURL: String let actionCallback: (Action) -> Void @State private var migrationConfirmed: Bool = false @State private var termsAccepted: Bool = false var body: some View { VStack(alignment: .leading, spacing: 56) { - Text(String.localized(key: "individualToTeam.confirmation.body", bundle: .module)) - .wireTextStyle(.body1) + VStack(alignment: .leading) { + HStack(alignment: .top, spacing: 8) { + Text(verbatim: "•") + Text(String.localized(key: "individualToTeam.confirmation.body.createTeam", bundle: .module)) + } + HStack(alignment: .top, spacing: 8) { + Text(verbatim: "•") + Text(String.localized(key: "individualToTeam.confirmation.body.teamOwner", bundle: .module)) + } + HStack(alignment: .top, spacing: 8) { + Text(verbatim: "•") + Text(String.localized(key: "individualToTeam.confirmation.body.permanent", bundle: .module)) + } + } + .wireTextStyle(.body1) VStack(alignment: .leading, spacing: 16) { Checkbox( isChecked: $migrationConfirmed, @@ -41,7 +56,12 @@ struct ConfirmationView: View { ) Checkbox( isChecked: $termsAccepted, - title: .localizedMarkdown(key: "individualToTeam.confirmation.termsCheckbox", bundle: .module) + title: .formattedMarkdown( + key: "individualToTeam.confirmation.termsCheckbox", + bundle: .module, + termsOfUseURL, + privacyPolicyURL + ) ) } Spacer() diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/ConfirmationPreview.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/ConfirmationPreview.swift index b344ec7cf8f..7df3588d1a0 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/ConfirmationPreview.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/ConfirmationPreview.swift @@ -25,7 +25,10 @@ import WireFoundation func confirmationPreview() -> some View { PageContainer( content: { - ConfirmationView { _ in } + ConfirmationView( + termsOfUseURL: "https://wire.com/en/terms-of-use-business", + privacyPolicyURL: "https://wire.com/privacy-policy" + ) { _ in } }, step: 3, stepCount: 4, diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/_FeaturePreview.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/_FeaturePreview.swift index 04419dc7abd..c917cd46960 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/_FeaturePreview.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/PreviewsSetup/_FeaturePreview.swift @@ -54,6 +54,8 @@ private struct FeaturePreviewContainer: UIViewControllerRepresentable { IndividualToTeamMigrationViewController( features: features, + privacyPolicyURL: "https://wire.com/privacy-policy", + termsOfUseURL: "https://wire.com/en/terms-of-use-business", useCase: MockUseCase.success(), userProfileName: "Some User", actionCallback: { _ in } diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamNameView.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamNameView.swift index 5c4f5255dd3..a7934981a6a 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamNameView.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamNameView.swift @@ -35,14 +35,19 @@ struct TeamNameView: View { .wireTextStyle(.body1) Spacer() .frame(height: 24) - Text(String.localized(key: "individualToTeam.teamName.field.title", bundle: .module)) - .wireTextStyle(.h4) + ( + Text(String.localized(key: "individualToTeam.teamName.field.title", bundle: .module)) + + Text(verbatim: " *") + .foregroundColor(ColorTheme.Base.requiredField.color) + ) + .wireTextStyle(.h4) TextField( String.localized(key: "individualToTeam.teamName.field.placeholder", bundle: .module), text: $teamName ) .textFieldStyle(.roundedBorder) .wireTextStyle(.body1) + Spacer() Button( diff --git a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamPlanSelectionView.swift b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamPlanSelectionView.swift index 40d77885dd1..1ba54de872d 100644 --- a/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamPlanSelectionView.swift +++ b/WireUI/Sources/WireIndividualToTeamMigrationUI/Views/TeamPlanSelectionView.swift @@ -53,7 +53,9 @@ struct TeamPlanSelectionView: View { } } Button( - action: {}, + action: { + actionCallback(.goToPlans) + }, label: { Text(String.localized(key: "individualToTeam.planSelection.url", bundle: .module)) .lineLimit(nil) diff --git a/wire-ios-sync-engine/Source/Data Model/Typing.swift b/wire-ios-sync-engine/Source/Data Model/Typing.swift index 4514f87f6af..2881bbb72e1 100644 --- a/wire-ios-sync-engine/Source/Data Model/Typing.swift +++ b/wire-ios-sync-engine/Source/Data Model/Typing.swift @@ -119,7 +119,7 @@ extension Typing: ZMTimerClient { self.sendNotification(for: conversation) } } catch { - WireLogger.updateEvent.error(("Failed to retrieve conversation object locally \(error)")) + WireLogger.updateEvent.error("Failed to retrieve conversation object locally \(error)") } } diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+IndividualToTeamMigration.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+IndividualToTeamMigration.swift index bed7bd73cf9..8dbb157c46d 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+IndividualToTeamMigration.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+IndividualToTeamMigration.swift @@ -27,9 +27,13 @@ public extension ZMUserSession { assertionFailure("apiService is nil") return nil } + + let builder = AccountsAPIBuilder(apiService: apiService) + let accountsAPI = builder.makeAPI(for: apiVersion) + return IndividualToTeamMigrationUseCaseImplementation( - apiService: apiService, - apiVersion: apiVersion + accountsAPI: accountsAPI, + context: syncContext ) } } diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift index d7427b4bea9..ec2114e6102 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift @@ -45,7 +45,13 @@ public final class ZMUserSession: NSObject { private(set) var coreDataStack: CoreDataStack! private let apiServiceFactory: APIServiceFactory - private(set) var apiService: APIServiceProtocol? + var apiService: APIServiceProtocol? { + guard let clientId = selfUserClient?.remoteIdentifier else { + return nil + } + return apiServiceFactory(clientId, userId) + } + let application: ZMApplication let flowManager: FlowManagerType private(set) var mediaManager: MediaManagerType @@ -1092,11 +1098,6 @@ extension ZMUserSession: ZMSyncStateDelegate { let clientId = userClient.safeRemoteIdentifier.safeForLoggingDescription WireLogger.authentication.addTag(.selfClientId, value: clientId) - guard let selfUserId = ZMUser.selfUser(in: syncContext).remoteIdentifier else { - assertionFailure("unable to find selfUser from syncContext,") - return - } - apiService = apiServiceFactory(clientId, selfUserId) } public func didFailToRegisterSelfUserClient(error: Error) { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift index 7be79f380a4..87cc54a049a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift @@ -266,6 +266,8 @@ final class SelfProfileViewController: UIViewController { private func userDidTapCreateTeam(useCase: IndividualToTeamMigrationUseCase, userName: String) { let vc = IndividualToTeamMigrationViewController( + privacyPolicyURL: WireURLs.shared.privacyPolicy.absoluteString, + termsOfUseURL: WireURLs.shared.legal.absoluteString, useCase: useCase, userProfileName: userName, actionCallback: { [weak self] action in @@ -275,6 +277,8 @@ final class SelfProfileViewController: UIViewController { switch action { case .cancel: presentedViewController?.dismiss(animated: true) + case .toLearnMoreAboutPlans: + _ = WireURLs.shared.wireEnterpriseInfo.open() case .completionGoToApp: dismissIndividualToTeamMigrationBanner() presentedViewController?.dismiss(animated: true) @@ -301,7 +305,7 @@ final class SelfProfileViewController: UIViewController { } private func navigateToTeam() { - // TODO: [WPB-11968] navigate to team + URL.manageTeam(source: .settings).open() } @objc diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewsMonitor.swift b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewsMonitor.swift index 7d62c4d7263..687d7ed900f 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewsMonitor.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewsMonitor.swift @@ -17,8 +17,8 @@ // import WireFoundation -import WireSyncEngine import WireLogging +import WireSyncEngine protocol SelfProfileViewsMonitor { var didViewSelfProfile: Bool { get }