diff --git a/BitwardenShared/Core/Auth/Models/Response/CreateAccountResponseModel.swift b/BitwardenShared/Core/Auth/Models/Response/CreateAccountResponseModel.swift index 4735e21fe..c7d6799a9 100644 --- a/BitwardenShared/Core/Auth/Models/Response/CreateAccountResponseModel.swift +++ b/BitwardenShared/Core/Auth/Models/Response/CreateAccountResponseModel.swift @@ -6,8 +6,6 @@ import Networking /// The response returned from the API upon creating an account. /// struct CreateAccountResponseModel: JSONResponse { - static let decoder = JSONDecoder() - // MARK: Properties /// The captcha bypass token returned in this response. diff --git a/BitwardenShared/Core/Auth/Models/Response/IdentityTokenResponseModel.swift b/BitwardenShared/Core/Auth/Models/Response/IdentityTokenResponseModel.swift index 12732663c..3ff053355 100644 --- a/BitwardenShared/Core/Auth/Models/Response/IdentityTokenResponseModel.swift +++ b/BitwardenShared/Core/Auth/Models/Response/IdentityTokenResponseModel.swift @@ -9,7 +9,7 @@ struct IdentityTokenResponseModel: Equatable, JSONResponse, KdfConfigProtocol { // MARK: Account Properties /// Whether the app needs to force a password reset. - let forcePasswordReset: Bool + @DefaultFalse var forcePasswordReset: Bool /// The type of KDF algorithm to use. let kdf: KdfType diff --git a/BitwardenShared/Core/Auth/Models/Response/KnownDeviceResponseModel.swift b/BitwardenShared/Core/Auth/Models/Response/KnownDeviceResponseModel.swift index 650f2539f..09f95aaa0 100644 --- a/BitwardenShared/Core/Auth/Models/Response/KnownDeviceResponseModel.swift +++ b/BitwardenShared/Core/Auth/Models/Response/KnownDeviceResponseModel.swift @@ -5,8 +5,6 @@ import Networking /// An object containing a value defining if this device has previously logged into this account or not. struct KnownDeviceResponseModel: JSONResponse { - static let decoder = JSONDecoder() - // MARK: Properties /// A flag indicating if this device is known or not. diff --git a/BitwardenShared/Core/Auth/Models/Response/PreLoginResponseModel.swift b/BitwardenShared/Core/Auth/Models/Response/PreLoginResponseModel.swift index d38a9a174..9e30de631 100644 --- a/BitwardenShared/Core/Auth/Models/Response/PreLoginResponseModel.swift +++ b/BitwardenShared/Core/Auth/Models/Response/PreLoginResponseModel.swift @@ -8,10 +8,6 @@ import Networking /// Contains information necessary for encrypting the user's password for login. /// struct PreLoginResponseModel: Equatable, JSONResponse, KdfConfigProtocol { - // MARK: Static Properties - - static let decoder = JSONDecoder() - // MARK: Properties /// The type of kdf algorithm to use for encryption. diff --git a/BitwardenShared/Core/Auth/Models/Response/PreValidateSingleSignOnResponse.swift b/BitwardenShared/Core/Auth/Models/Response/PreValidateSingleSignOnResponse.swift index 5dc6ca2d7..5fe6320c1 100644 --- a/BitwardenShared/Core/Auth/Models/Response/PreValidateSingleSignOnResponse.swift +++ b/BitwardenShared/Core/Auth/Models/Response/PreValidateSingleSignOnResponse.swift @@ -6,8 +6,6 @@ import Networking /// The response returned from the API upon pre-validating the single-sign on. /// struct PreValidateSingleSignOnResponse: JSONResponse, Equatable { - static let decoder = JSONDecoder() - // MARK: Properties /// The token returned in this response. diff --git a/BitwardenShared/Core/Auth/Models/Response/RegisterFinishResponseModel.swift b/BitwardenShared/Core/Auth/Models/Response/RegisterFinishResponseModel.swift index 74acbae28..1a73a6488 100644 --- a/BitwardenShared/Core/Auth/Models/Response/RegisterFinishResponseModel.swift +++ b/BitwardenShared/Core/Auth/Models/Response/RegisterFinishResponseModel.swift @@ -6,8 +6,6 @@ import Networking /// The response returned from the API upon creating an account. /// struct RegisterFinishResponseModel: JSONResponse { - static let decoder = JSONDecoder() - // MARK: Properties /// The captcha bypass token returned in this response. diff --git a/BitwardenShared/Core/Auth/Services/AuthService.swift b/BitwardenShared/Core/Auth/Services/AuthService.swift index 9cf34d9cc..185a2db3e 100644 --- a/BitwardenShared/Core/Auth/Services/AuthService.swift +++ b/BitwardenShared/Core/Auth/Services/AuthService.swift @@ -504,7 +504,7 @@ class DefaultAuthService: AuthService { // swiftlint:disable:this type_body_leng let appId = await appIdService.getOrCreateAppId() // Initiate the login request and cache the result. - let loginWithDeviceData = try await clientService.auth().newAuthRequest(email: email) + let loginWithDeviceData = try await clientService.auth(isPreAuth: true).newAuthRequest(email: email) let loginRequest = try await authAPIService.initiateLoginWithDevice(LoginWithDeviceRequestModel( email: email, publicKey: loginWithDeviceData.publicKey, diff --git a/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift b/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift index acc00f59e..1ec1cc197 100644 --- a/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift +++ b/BitwardenShared/Core/Auth/Services/AuthServiceTests.swift @@ -272,6 +272,7 @@ class AuthServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body_ // Verify the results. XCTAssertEqual(client.requests.count, 1) XCTAssertEqual(clientService.mockAuth.newAuthRequestEmail, "email@example.com") + XCTAssertTrue(clientService.mockAuthIsPreAuth) XCTAssertEqual(result.authRequestResponse, authRequestResponse) XCTAssertEqual(result.requestId, LoginRequest.fixture().id) } diff --git a/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrls.swift b/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrls.swift index eb6c42a77..512aeb0d0 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrls.swift +++ b/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrls.swift @@ -42,6 +42,13 @@ extension EnvironmentUrls { /// - Parameter environmentUrlData: The environment URLs used to initialize `EnvironmentUrls`. /// init(environmentUrlData: EnvironmentUrlData) { + // Use the default URLs if the region matches US or EU. + let environmentUrlData: EnvironmentUrlData = switch environmentUrlData.region { + case .europe: .defaultEU + case .unitedStates: .defaultUS + case .selfHosted: environmentUrlData + } + if environmentUrlData.region == .selfHosted, let base = environmentUrlData.base { apiURL = base.appendingPathComponent("api") baseURL = base diff --git a/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrlsTests.swift b/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrlsTests.swift index 60df7b966..aca3202e3 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrlsTests.swift +++ b/BitwardenShared/Core/Platform/Models/Domain/EnvironmentUrlsTests.swift @@ -72,6 +72,50 @@ class EnvironmentUrlsTests: BitwardenTestCase { ) } + /// `init(environmentUrlData:)` defaults to the pre-defined EU URLs if the base URL matches the EU environment. + func test_init_environmentUrlData_baseUrl_europe() { + let subject = EnvironmentUrls( + environmentUrlData: EnvironmentUrlData(base: URL(string: "https://vault.bitwarden.eu")!) + ) + XCTAssertEqual( + subject, + EnvironmentUrls( + apiURL: URL(string: "https://api.bitwarden.eu")!, + baseURL: URL(string: "https://vault.bitwarden.eu")!, + eventsURL: URL(string: "https://events.bitwarden.eu")!, + iconsURL: URL(string: "https://icons.bitwarden.eu")!, + identityURL: URL(string: "https://identity.bitwarden.eu")!, + importItemsURL: URL(string: "https://vault.bitwarden.eu/#/tools/import")!, + recoveryCodeURL: URL(string: "https://vault.bitwarden.eu/#/recover-2fa")!, + sendShareURL: URL(string: "https://vault.bitwarden.eu/#/send")!, + settingsURL: URL(string: "https://vault.bitwarden.eu/#/settings")!, + webVaultURL: URL(string: "https://vault.bitwarden.eu")! + ) + ) + } + + /// `init(environmentUrlData:)` defaults to the pre-defined US URLs if the base URL matches the US environment. + func test_init_environmentUrlData_baseUrl_unitedStates() { + let subject = EnvironmentUrls( + environmentUrlData: EnvironmentUrlData(base: URL(string: "https://vault.bitwarden.com")!) + ) + XCTAssertEqual( + subject, + EnvironmentUrls( + apiURL: URL(string: "https://api.bitwarden.com")!, + baseURL: URL(string: "https://vault.bitwarden.com")!, + eventsURL: URL(string: "https://events.bitwarden.com")!, + iconsURL: URL(string: "https://icons.bitwarden.net")!, + identityURL: URL(string: "https://identity.bitwarden.com")!, + importItemsURL: URL(string: "https://vault.bitwarden.com/#/tools/import")!, + recoveryCodeURL: URL(string: "https://vault.bitwarden.com/#/recover-2fa")!, + sendShareURL: URL(string: "https://send.bitwarden.com/#")!, + settingsURL: URL(string: "https://vault.bitwarden.com/#/settings")!, + webVaultURL: URL(string: "https://vault.bitwarden.com")! + ) + ) + } + /// `init(environmentUrlData:)` sets the URLs from the base URL which includes a trailing slash. func test_init_environmentUrlData_baseUrlWithTrailingSlash() { let subject = EnvironmentUrls( diff --git a/BitwardenShared/Core/Platform/Models/Domain/ServerConfig.swift b/BitwardenShared/Core/Platform/Models/Domain/ServerConfig.swift index 9237a3b4b..9466b7e1f 100644 --- a/BitwardenShared/Core/Platform/Models/Domain/ServerConfig.swift +++ b/BitwardenShared/Core/Platform/Models/Domain/ServerConfig.swift @@ -29,10 +29,10 @@ struct ServerConfig: Equatable, Codable, Sendable { environment = responseModel.environment.map(EnvironmentServerConfig.init) self.date = date let features: [(FeatureFlag, AnyCodable)] - features = responseModel.featureStates.compactMap { key, value in + features = responseModel.featureStates?.compactMap { key, value in guard let flag = FeatureFlag(rawValue: key) else { return nil } return (flag, value) - } + } ?? [] featureStates = Dictionary(uniqueKeysWithValues: features) gitHash = responseModel.gitHash @@ -43,7 +43,9 @@ struct ServerConfig: Equatable, Codable, Sendable { // MARK: Methods /// Whether the server supports cipher key encryption. + /// /// - Returns: `true` if it's supported, `false` otherwise. + /// func supportsCipherKeyEncryption() -> Bool { guard let minVersion = ServerVersion(Constants.cipherKeyEncryptionMinServerVersion), let serverVersion = ServerVersion(version), @@ -52,6 +54,14 @@ struct ServerConfig: Equatable, Codable, Sendable { } return true } + + /// Checks if the server is an official Bitwarden server. + /// + /// - Returns: `true` if the server is `nil`, indicating an official Bitwarden server, otherwise `false`. + /// + func isOfficialBitwardenServer() -> Bool { + server == nil + } } // MARK: - ThirdPartyServerConfig diff --git a/BitwardenShared/Core/Platform/Services/API/Extensions/JSONResponse+Bitwarden.swift b/BitwardenShared/Core/Platform/Services/API/Extensions/JSONResponse+Bitwarden.swift index a93fdca82..58955b93c 100644 --- a/BitwardenShared/Core/Platform/Services/API/Extensions/JSONResponse+Bitwarden.swift +++ b/BitwardenShared/Core/Platform/Services/API/Extensions/JSONResponse+Bitwarden.swift @@ -3,5 +3,5 @@ import Networking extension JSONResponse { /// The decoder used by default to decode JSON responses from the API. - static var decoder: JSONDecoder { .defaultDecoder } + static var decoder: JSONDecoder { .pascalOrSnakeCaseDecoder } } diff --git a/BitwardenShared/Core/Platform/Services/API/Response/ConfigResponseModel.swift b/BitwardenShared/Core/Platform/Services/API/Response/ConfigResponseModel.swift index 467ba61cd..c2100a34c 100644 --- a/BitwardenShared/Core/Platform/Services/API/Response/ConfigResponseModel.swift +++ b/BitwardenShared/Core/Platform/Services/API/Response/ConfigResponseModel.swift @@ -12,7 +12,7 @@ struct ConfigResponseModel: Equatable, JSONResponse { let environment: EnvironmentServerConfigResponseModel? /// Feature flags to configure the client. - let featureStates: [String: AnyCodable] + let featureStates: [String: AnyCodable]? /// The git hash of the server. let gitHash: String diff --git a/BitwardenShared/Core/Platform/Utilities/DefaultFalse.swift b/BitwardenShared/Core/Platform/Utilities/DefaultFalse.swift new file mode 100644 index 000000000..7ba62b23a --- /dev/null +++ b/BitwardenShared/Core/Platform/Utilities/DefaultFalse.swift @@ -0,0 +1,47 @@ +/// A property wrapper that will default the wrapped value to `false` if decoding fails. This is +/// useful for decoding a boolean value which may not be present in the response. +/// +@propertyWrapper +struct DefaultFalse: Codable, Hashable { + // MARK: Properties + + /// The wrapped value. + let wrappedValue: Bool + + // MARK: Initialization + + /// Initialize a `DefaultFalse` with a wrapped value. + /// + /// - Parameter wrappedValue: The value that is contained in the property wrapper. + /// + init(wrappedValue: Bool) { + self.wrappedValue = wrappedValue + } + + // MARK: Decodable + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try container.decode(Bool.self) + } + + // MARK: Encodable + + func encode(to encoder: Encoder) throws { + try wrappedValue.encode(to: encoder) + } +} + +// MARK: - KeyedDecodingContainer + +extension KeyedDecodingContainer { + /// When decoding a `DefaultFalse` wrapped value, if the property doesn't exist, default to `false`. + /// + /// - Parameters: + /// - type: The type of value to attempt to decode. + /// - key: The key used to decode the value. + /// + func decode(_ type: DefaultFalse.Type, forKey key: Key) throws -> DefaultFalse { + try decodeIfPresent(type, forKey: key) ?? DefaultFalse(wrappedValue: false) + } +} diff --git a/BitwardenShared/Core/Platform/Utilities/DefaultFalseTests.swift b/BitwardenShared/Core/Platform/Utilities/DefaultFalseTests.swift new file mode 100644 index 000000000..2bbaab0d9 --- /dev/null +++ b/BitwardenShared/Core/Platform/Utilities/DefaultFalseTests.swift @@ -0,0 +1,61 @@ +import XCTest + +@testable import BitwardenShared + +class DefaultFalseTests: BitwardenTestCase { + // MARK: Types + + struct Model: Codable, Equatable { + @DefaultFalse var value: Bool + } + + // MARK: Tests + + /// `DefaultFalse` encodes a `false` wrapped value. + func test_encode_false() throws { + let subject = Model(value: false) + let data = try JSONEncoder().encode(subject) + XCTAssertEqual(String(data: data, encoding: .utf8), #"{"value":false}"#) + } + + /// `DefaultFalse` encodes a `true` wrapped value. + func test_encode_true() throws { + let subject = Model(value: true) + let data = try JSONEncoder().encode(subject) + XCTAssertEqual(String(data: data, encoding: .utf8), #"{"value":true}"#) + } + + /// Decoding a `DefaultFalse` wrapped value will decode a `false` value from the JSON. + func test_decode_false() throws { + let json = #"{"value": true}"# + let data = try XCTUnwrap(json.data(using: .utf8)) + let subject = try JSONDecoder().decode(Model.self, from: data) + XCTAssertEqual(subject, Model(value: true)) + } + + /// Decoding a `DefaultFalse` wrapped value will default the value to `false` if the key is + /// missing from the JSON. + func test_decode_missing() throws { + let json = #"{}"# + let data = try XCTUnwrap(json.data(using: .utf8)) + let subject = try JSONDecoder().decode(Model.self, from: data) + XCTAssertEqual(subject, Model(value: false)) + } + + /// Decoding a `DefaultFalse` wrapped value will default the value to `false` if the value is + /// `null` in the JSON. + func test_decode_null() throws { + let json = #"{"value": null}"# + let data = try XCTUnwrap(json.data(using: .utf8)) + let subject = try JSONDecoder().decode(Model.self, from: data) + XCTAssertEqual(subject, Model(value: false)) + } + + /// Decoding a `DefaultFalse` wrapped value will decode a `true` value from the JSON. + func test_decode_true() throws { + let json = #"{"value": true}"# + let data = try XCTUnwrap(json.data(using: .utf8)) + let subject = try JSONDecoder().decode(Model.self, from: data) + XCTAssertEqual(subject, Model(value: true)) + } +} diff --git a/BitwardenShared/Core/Vault/Models/Response/ProfileResponseModel.swift b/BitwardenShared/Core/Vault/Models/Response/ProfileResponseModel.swift index dafa4729e..ab6f40932 100644 --- a/BitwardenShared/Core/Vault/Models/Response/ProfileResponseModel.swift +++ b/BitwardenShared/Core/Vault/Models/Response/ProfileResponseModel.swift @@ -39,7 +39,7 @@ struct ProfileResponseModel: Codable, Equatable { let premium: Bool /// Whether the user has a premium account from their organization. - let premiumFromOrganization: Bool + @DefaultFalse var premiumFromOrganization: Bool /// The user's private key. let privateKey: String? @@ -51,5 +51,5 @@ struct ProfileResponseModel: Codable, Equatable { let twoFactorEnabled: Bool /// Whether the user uses key connector. - let usesKeyConnector: Bool + @DefaultFalse var usesKeyConnector: Bool } diff --git a/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift b/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift index d5907d738..063994f7c 100644 --- a/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift +++ b/BitwardenShared/UI/Auth/Landing/LandingProcessor.swift @@ -102,14 +102,23 @@ class LandingProcessor: StateProcessor { & HasAuthRepository & HasAuthService & HasCaptchaService + & HasConfigService & HasDeviceAPIService & HasErrorReporter & HasPolicyService @@ -107,7 +108,7 @@ class LoginProcessor: StateProcessor { /// - Parameter siteKey: The site key that was returned with a captcha error. The token used to authenticate /// with hCaptcha. /// - private func launchCaptchaFlow(with siteKey: String) { + private func launchCaptchaFlow(with siteKey: String) async { do { let url = try services.captchaService.generateCaptchaUrl(with: siteKey) coordinator.navigate( @@ -118,8 +119,7 @@ class LoginProcessor: StateProcessor { context: self ) } catch { - coordinator.showAlert(.networkResponseError(error)) - services.errorReporter.log(error: error) + await handleErrorResponse(error) } } @@ -154,15 +154,14 @@ class LoginProcessor: StateProcessor { } catch let error as IdentityTokenRequestError { switch error { case let .captchaRequired(hCaptchaSiteCode): - launchCaptchaFlow(with: hCaptchaSiteCode) + await launchCaptchaFlow(with: hCaptchaSiteCode) case let .twoFactorRequired(authMethodsData, _, _): coordinator.navigate( to: .twoFactor(state.username, .password(state.masterPassword), authMethodsData, nil) ) } } catch { - coordinator.showAlert(.networkResponseError(error)) - services.errorReporter.log(error: error) + await handleErrorResponse(error) } } @@ -185,10 +184,29 @@ class LoginProcessor: StateProcessor { ) state.isLoginWithDeviceVisible = isKnownDevice } catch { - coordinator.showAlert(.networkResponseError(error)) - services.errorReporter.log(error: error) + await handleErrorResponse(error) } } + + /// Handles network error responses. + /// + /// Determines whether the Bitwarden server is official or unofficial and passes this information + /// along with the error to the coordinator to display an appropriate alert on the main thread. + /// The error is also logged using the error reporter. + /// + /// - Parameter error: The error received from the network request. + /// + private func handleErrorResponse(_ error: Error) async { + services.errorReporter.log(error: error) + let serverConfig = await services.configService.getConfig(isPreAuth: true) + let isOfficialBitwardenServer = serverConfig?.isOfficialBitwardenServer() ?? true + coordinator.showAlert( + .networkResponseError( + error, + isOfficialBitwardenServer: isOfficialBitwardenServer + ) + ) + } } // MARK: CaptchaFlowDelegate @@ -203,12 +221,12 @@ extension LoginProcessor: CaptchaFlowDelegate { func captchaErrored(error: Error) { guard (error as NSError).code != ASWebAuthenticationSessionError.canceledLogin.rawValue else { return } - services.errorReporter.log(error: error) - // Show the alert after a delay to ensure it doesn't try to display over the // closing captcha view. DispatchQueue.main.asyncAfter(deadline: UI.after(0.6)) { - self.coordinator.showAlert(.networkResponseError(error)) + Task { + await self.handleErrorResponse(error) + } } } } diff --git a/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift b/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift index 00ac8efbb..256fc21c4 100644 --- a/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift +++ b/BitwardenShared/UI/Auth/Login/LoginProcessorTests.swift @@ -4,6 +4,8 @@ import XCTest @testable import BitwardenShared +// swiftlint:disable file_length + // MARK: - LoginProcessorTests class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_body_length @@ -13,6 +15,7 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo var authRepository: MockAuthRepository! var authService: MockAuthService! var captchaService: MockCaptchaService! + var configService: MockConfigService! var client: MockHTTPClient! var coordinator: MockCoordinator! var errorReporter: MockErrorReporter! @@ -27,6 +30,7 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo authRepository = MockAuthRepository() authService = MockAuthService() captchaService = MockCaptchaService() + configService = MockConfigService() client = MockHTTPClient() coordinator = MockCoordinator() errorReporter = MockErrorReporter() @@ -41,6 +45,7 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo authRepository: authRepository, authService: authService, captchaService: captchaService, + configService: configService, errorReporter: errorReporter, httpClient: client ), @@ -55,6 +60,7 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo authRepository = nil authService = nil captchaService = nil + configService = nil client = nil coordinator = nil errorReporter = nil @@ -107,7 +113,94 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo XCTAssertFalse(coordinator.isLoadingOverlayShowing) XCTAssertEqual(coordinator.loadingOverlaysShown, [.init(title: Localizations.loading)]) XCTAssertFalse(subject.state.isLoginWithDeviceVisible) - // TODO: BIT-709 Add assertion for error state. + XCTAssertEqual(coordinator.alertShown.last, .networkResponseError(BitwardenTestError.example)) + XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) + } + + /// `perform(_:)` with `.appeared` and an error occurs with an unofficial server and the error isn't expected. + @MainActor + func test_perform_appeared_failure_unofficialServer() async throws { + configService.configMocker.withResult( + ServerConfig( + date: Date(year: 2024, month: 2, day: 14, hour: 7, minute: 50, second: 0), + responseModel: ConfigResponseModel( + environment: nil, + featureStates: [:], + gitHash: "75238191", + server: .init(name: "Vaultwarden", url: "example.com"), + version: "2024.4.0" + ) + ) + ) + subject.state.isLoginWithDeviceVisible = false + client.results = [ + .httpFailure(BitwardenTestError.example), + ] + await subject.perform(.appeared) + + XCTAssertFalse(coordinator.isLoadingOverlayShowing) + XCTAssertEqual(coordinator.loadingOverlaysShown, [.init(title: Localizations.loading)]) + XCTAssertFalse(subject.state.isLoginWithDeviceVisible) + XCTAssertEqual( + coordinator.alertShown.last, + .networkResponseError( + BitwardenTestError.example, + isOfficialBitwardenServer: false + ) + ) + XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) + } + + /// `perform(_:)` with `.appeared` and an error occurs with an unofficial server but the error is expected. + @MainActor + func test_perform_appeared_failure_supportedErrorWithUnofficialServer() async throws { + configService.configMocker.withResult( + ServerConfig( + date: Date(year: 2024, month: 2, day: 14, hour: 7, minute: 50, second: 0), + responseModel: ConfigResponseModel( + environment: nil, + featureStates: [:], + gitHash: "75238191", + server: .init(name: "Vaultwarden", url: "example.com"), + version: "2024.4.0" + ) + ) + ) + subject.state.isLoginWithDeviceVisible = false + + let validationResponse = ResponseValidationErrorModel( + error: "Invalid credentials", + errorDescription: "an error occured", + errorModel: .init( + message: "message", + object: "object" + ) + ) + + client.results = [ + .httpFailure( + ServerError.validationError( + validationErrorResponse: validationResponse + ) + ), + ] + + await subject.perform(.appeared) + + XCTAssertFalse(coordinator.isLoadingOverlayShowing) + XCTAssertEqual(coordinator.loadingOverlaysShown, [.init(title: Localizations.loading)]) + XCTAssertFalse(subject.state.isLoginWithDeviceVisible) + XCTAssertEqual( + coordinator.alertShown.last, + .networkResponseError( + ServerError.validationError(validationErrorResponse: validationResponse), + isOfficialBitwardenServer: false + ) + ) + XCTAssertEqual( + errorReporter.errors.last as? ServerError, + .validationError(validationErrorResponse: validationResponse) + ) } /// `perform(_:)` with `.appeared` and a true result shows the login with device button. @@ -262,6 +355,42 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) } + /// `perform(_:)` with `.loginWithMasterPasswordPressed` and a captcha flow error shows an unofficial server error. + @MainActor + func test_perform_loginWithMasterPasswordPressed_captchaFlowError_unofficialServer() async { + configService.configMocker.withResult( + ServerConfig( + date: Date(year: 2024, month: 2, day: 14, hour: 7, minute: 50, second: 0), + responseModel: ConfigResponseModel( + environment: nil, + featureStates: [:], + gitHash: "75238191", + server: .init(name: "Vaultwarden", url: "example.com"), + version: "2024.4.0" + ) + ) + ) + subject.state.masterPassword = "Test" + authService.loginWithMasterPasswordResult = .failure( + IdentityTokenRequestError.captchaRequired(hCaptchaSiteCode: "token") + ) + captchaService.generateCaptchaUrlResult = .failure(BitwardenTestError.example) + + await subject.perform(.loginWithMasterPasswordPressed) + + XCTAssertEqual(authService.loginWithMasterPasswordPassword, "Test") + XCTAssertEqual(captchaService.generateCaptchaSiteKey, "token") + + XCTAssertEqual( + coordinator.alertShown.last, + .networkResponseError( + BitwardenTestError.example, + isOfficialBitwardenServer: false + ) + ) + XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) + } + /// `perform(_:)` with `.loginWithMasterPasswordPressed` records non captcha errors. @MainActor func test_perform_loginWithMasterPasswordPressed_error() async throws { @@ -276,6 +405,37 @@ class LoginProcessorTests: BitwardenTestCase { // swiftlint:disable:this type_bo XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) } + /// `perform(_:)` with `.loginWithMasterPasswordPressed` records an error for an unofficial bitwarden server. + @MainActor + func test_perform_loginWithMasterPasswordPressed_unofficialBitwardenServer() async throws { + configService.configMocker.withResult( + ServerConfig( + date: Date(year: 2024, month: 2, day: 14, hour: 7, minute: 50, second: 0), + responseModel: ConfigResponseModel( + environment: nil, + featureStates: [:], + gitHash: "75238191", + server: .init(name: "Vaultwarden", url: "example.com"), + version: "2024.4.0" + ) + ) + ) + subject.state.masterPassword = "Test" + authService.loginWithMasterPasswordResult = .failure(BitwardenTestError.example) + + await subject.perform(.loginWithMasterPasswordPressed) + + XCTAssertEqual(authService.loginWithMasterPasswordPassword, "Test") + XCTAssertEqual( + coordinator.alertShown.last, + .networkResponseError( + BitwardenTestError.example, + isOfficialBitwardenServer: false + ) + ) + XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) + } + /// `perform(_:)` with `.loginWithMasterPasswordPressed` shows an alert for empty login text. @MainActor func test_perform_loginWithMasterPasswordPressed_invalidInput() async throws { diff --git a/BitwardenShared/UI/Platform/Application/Appearance/UI.swift b/BitwardenShared/UI/Platform/Application/Appearance/UI.swift index 6de5d17fa..86e0c7d07 100644 --- a/BitwardenShared/UI/Platform/Application/Appearance/UI.swift +++ b/BitwardenShared/UI/Platform/Application/Appearance/UI.swift @@ -104,8 +104,7 @@ public enum UI { UISearchBar.appearance().setImage(tintedImage, for: .clear, state: .normal) UISearchBar.appearance().setImage(Asset.Images.magnifyingGlass.image, for: .search, state: .normal) - // Adjust the appearance of `UITextView` for `BitwardenMultilineTextField` instances on - // iOS 15. + // Adjust the appearance of `UITextView` for `BitwardenUITextField` instances on iOS 15. UITextView.appearance().isScrollEnabled = false UITextView.appearance().backgroundColor = .clear UITextView.appearance().textContainerInset = .zero diff --git a/BitwardenShared/UI/Platform/Application/Extensions/Alert+Networking.swift b/BitwardenShared/UI/Platform/Application/Extensions/Alert+Networking.swift index 513874ac8..3d58d63f8 100644 --- a/BitwardenShared/UI/Platform/Application/Extensions/Alert+Networking.swift +++ b/BitwardenShared/UI/Platform/Application/Extensions/Alert+Networking.swift @@ -29,12 +29,14 @@ extension Alert { /// /// - Parameters: /// - error: The networking error that occurred. + /// - isOfficialBitwardenServer: Indicates whether the request was made to the official Bitwarden server /// - tryAgain: An action allowing the user to retry the request. /// /// - Returns: An alert notifying the user that a networking error occurred. /// static func networkResponseError( _ error: Error, + isOfficialBitwardenServer: Bool = true, _ tryAgain: (() async -> Void)? = nil ) -> Alert { switch error { @@ -64,7 +66,10 @@ extension Alert { ] ) default: - return defaultAlert(title: Localizations.anErrorHasOccurred) + return defaultAlert( + title: Localizations.anErrorHasOccurred, + message: isOfficialBitwardenServer ? nil : Localizations.thisIsNotARecognizedServerDescriptionLong + ) } } } diff --git a/BitwardenShared/UI/Platform/Application/Extensions/Alert+NetworkingTests.swift b/BitwardenShared/UI/Platform/Application/Extensions/Alert+NetworkingTests.swift index 80009ea4a..b28abbaf8 100644 --- a/BitwardenShared/UI/Platform/Application/Extensions/Alert+NetworkingTests.swift +++ b/BitwardenShared/UI/Platform/Application/Extensions/Alert+NetworkingTests.swift @@ -44,6 +44,21 @@ class AlertNetworkingTests: BitwardenTestCase { XCTAssertNil(action.handler) } + /// `.networkResponseError` builds an alert to display an error message for an unofficial Bitwarden server. + func test_networkResponseError_unofficialBitwardenServerError() { + let error = BitwardenTestError.example + let subject = Alert.networkResponseError(error, isOfficialBitwardenServer: false) + + XCTAssertEqual(subject.message, Localizations.thisIsNotARecognizedServerDescriptionLong) + XCTAssertEqual(subject.preferredStyle, .alert) + XCTAssertEqual(subject.alertActions.count, 1) + + let action = subject.alertActions[0] + XCTAssertEqual(action.title, Localizations.ok) + XCTAssertEqual(action.style, .cancel) + XCTAssertNil(action.handler) + } + /// Tests the `internetConnectionError` alert contains the correct properties. func test_noInternetConnection() { let urlError = URLError(.notConnectedToInternet) diff --git a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings index 66e2501b3..c18b518b6 100644 --- a/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings +++ b/BitwardenShared/UI/Platform/Application/Support/Localizations/en.lproj/Localizable.strings @@ -1002,3 +1002,4 @@ "AutoFillActivatedDescriptionLong" = "You can now use autofill to log into apps and websites using your saved passwords. Now, you can explore everything else Bitwarden has to offer."; "GetStarted" = "Get started"; "Dismiss" = "Dismiss"; +"ThisIsNotARecognizedServerDescriptionLong" = "This is not a recognized Bitwarden server. You may need to check with your provider or update your server."; diff --git a/BitwardenShared/UI/Platform/Application/Views/BitwardenMultilineTextField.swift b/BitwardenShared/UI/Platform/Application/Views/BitwardenMultilineTextField.swift deleted file mode 100644 index 86099979d..000000000 --- a/BitwardenShared/UI/Platform/Application/Views/BitwardenMultilineTextField.swift +++ /dev/null @@ -1,77 +0,0 @@ -import SwiftUI - -// MARK: - BitwardenMultilineTextField - -/// A variant of the standard text field used within this application that allows for multiple lines -/// of text displayed vertically. -/// -/// - Note: iOS 15 uses a `TextEditor` instead of `TextField` to render the text. See -/// `UI.applyDefaultAppearances()` for iOS 15 specific styling. -/// -struct BitwardenMultilineTextField: View { - // MARK: Properties - - /// The accessibility identifier for the text field. - let accessibilityIdentifier: String? - - /// The title of the text field. - let title: String? - - /// The footer text displayed below the text field. - let footer: String? - - /// The text entered into the text field. - @Binding var text: String - - // MARK: View - - var body: some View { - BitwardenField( - title: title, - footer: footer - ) { - if #available(iOSApplicationExtension 16, *) { - TextField( - "", - text: $text, - axis: .vertical - ) - .styleGuide(.body, includeLineSpacing: false) - .accessibilityIdentifier(accessibilityIdentifier ?? "") - .foregroundStyle(Asset.Colors.textPrimary.swiftUIColor) - .tint(Asset.Colors.tintPrimary.swiftUIColor) - .scrollDisabled(true) - } else { - TextEditor( - text: $text - ) - .styleGuide(.body, includeLineSpacing: false) - .accessibilityIdentifier(accessibilityIdentifier ?? "") - .foregroundStyle(Asset.Colors.textPrimary.swiftUIColor) - .tint(Asset.Colors.tintPrimary.swiftUIColor) - } - } - } - - // MARK: Initialization - - /// Initializes a new `BitwardenMultilineTextField`. - /// - /// - Parameters: - /// - title: The title of the text field. - /// - text: The text entered into the text field. - /// - footer: The footer text displayed below the text field. - /// - accessibilityIdentifier: The accessibility identifier for the text field. - /// - init( - title: String? = nil, - text: Binding, - footer: String? = nil, - accessibilityIdentifier: String? = nil - ) { - self.accessibilityIdentifier = accessibilityIdentifier - self.title = title - self.footer = footer - _text = text - } -} diff --git a/BitwardenShared/UI/Platform/Application/Views/BitwardenTextValueField.swift b/BitwardenShared/UI/Platform/Application/Views/BitwardenTextValueField.swift index 3ce330d22..f5f8a2884 100644 --- a/BitwardenShared/UI/Platform/Application/Views/BitwardenTextValueField.swift +++ b/BitwardenShared/UI/Platform/Application/Views/BitwardenTextValueField.swift @@ -15,6 +15,10 @@ struct BitwardenTextValueField: View where AccessoryContent: V /// The (optional) accessibility identifier to apply to the title of the field (if it exists) var titleAccessibilityIdentifier: String? + /// A flag to determine whether to use a `UITextView` implementation instead of the default SwiftUI-based text view. + /// When `true`, a `UITextView` will be used for improved text selection and cursor/keyboard management. + var useUIKitTextView: Bool + /// The text value to display in this field. var value: String @@ -25,19 +29,37 @@ struct BitwardenTextValueField: View where AccessoryContent: V /// content automatically has the `AccessoryButtonStyle` applied to it. var accessoryContent: AccessoryContent? + /// A state variable that holds the dynamic height of the text view. + /// This value is updated based on the content size of the text view, + /// allowing for automatic resizing to fit the text content. + /// The initial height is set to a default value of 28 points. + @SwiftUI.State private var textViewDynamicHeight: CGFloat = 28 + // MARK: View var body: some View { - BitwardenField(title: title, titleAccessibilityIdentifier: titleAccessibilityIdentifier) { - Text(value) - .styleGuide(.body) - .multilineTextAlignment(.leading) - .foregroundColor(Asset.Colors.textPrimary.swiftUIColor) - .accessibilityIdentifier(valueAccessibilityIdentifier ?? value) - .if(textSelectionEnabled) { textView in - textView - .textSelection(.enabled) - } + BitwardenField( + title: title, + titleAccessibilityIdentifier: titleAccessibilityIdentifier + ) { + if useUIKitTextView { + BitwardenUITextView( + text: .constant(value), + calculatedHeight: $textViewDynamicHeight, + isEditable: false + ) + .frame(minHeight: textViewDynamicHeight) + } else { + Text(value) + .styleGuide(.body) + .multilineTextAlignment(.leading) + .foregroundColor(Asset.Colors.textPrimary.swiftUIColor) + .accessibilityIdentifier(valueAccessibilityIdentifier ?? value) + .if(textSelectionEnabled) { textView in + textView + .textSelection(.enabled) + } + } } accessoryContent: { accessoryContent } @@ -54,7 +76,8 @@ struct BitwardenTextValueField: View where AccessoryContent: V /// - valueAccessibilityIdentifier: The (optional) accessibility identifier to apply /// to the displayed value of the field /// - textSelectionEnabled: Whether text selection is enabled. - /// This doesn't allow range selection, only copy/share actions. + /// This doesn't allow range selection, only copy/share actions. + /// - useUIKitTextView: Whether we should use a UITextView or a SwiftUI version. /// - accessoryContent: Any accessory content that should be displayed on the trailing edge of /// the field. This content automatically has the `AccessoryButtonStyle` applied to it. init( @@ -62,13 +85,15 @@ struct BitwardenTextValueField: View where AccessoryContent: V titleAccessibilityIdentifier: String? = "ItemName", value: String, valueAccessibilityIdentifier: String? = "ItemValue", - textSelectionEnabled: Bool = false, + textSelectionEnabled: Bool = true, + useUIKitTextView: Bool = false, @ViewBuilder accessoryContent: () -> AccessoryContent ) { self.textSelectionEnabled = textSelectionEnabled self.title = title self.titleAccessibilityIdentifier = titleAccessibilityIdentifier self.value = value + self.useUIKitTextView = useUIKitTextView self.valueAccessibilityIdentifier = valueAccessibilityIdentifier self.accessoryContent = accessoryContent() } @@ -85,21 +110,24 @@ extension BitwardenTextValueField where AccessoryContent == EmptyView { /// - valueAccessibilityIdentifier: The (optional) accessibility identifier to apply /// to the displayed value of the field /// - textSelectionEnabled: Whether text selection is enabled. - /// This doesn't allow range selection, only copy/share actions. + /// This doesn't allow range selection, only copy/share actions. + /// - useUIKitTextView: Whether we should use a UITextView or a SwiftUI version. /// init( title: String? = nil, titleAccessibilityIdentifier: String? = "ItemName", value: String, valueAccessibilityIdentifier: String? = "ItemValue", - textSelectionEnabled: Bool = false + textSelectionEnabled: Bool = true, + useUIKitTextView: Bool = false ) { self.init( title: title, titleAccessibilityIdentifier: titleAccessibilityIdentifier, value: value, valueAccessibilityIdentifier: valueAccessibilityIdentifier, - textSelectionEnabled: textSelectionEnabled + textSelectionEnabled: textSelectionEnabled, + useUIKitTextView: useUIKitTextView ) { EmptyView() } @@ -119,4 +147,16 @@ extension BitwardenTextValueField where AccessoryContent == EmptyView { } .background(Color(.systemGroupedBackground)) } + +#Preview("Legacy view") { + VStack { + BitwardenTextValueField( + title: "Title", + value: "Text field text", + useUIKitTextView: true + ) + .padding() + } + .background(Color(.systemGroupedBackground)) +} #endif diff --git a/BitwardenShared/UI/Platform/Application/Views/BitwardenUITextView.swift b/BitwardenShared/UI/Platform/Application/Views/BitwardenUITextView.swift new file mode 100644 index 000000000..4bbb160e0 --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Views/BitwardenUITextView.swift @@ -0,0 +1,134 @@ +import SwiftUI +import UIKit + +// MARK: - BitwardenUITextView + +/// A custom `UITextView` wrapped in a `UIViewRepresentable` for use in SwiftUI. +/// +struct BitwardenUITextView: UIViewRepresentable { + // MARK: - Coordinator + + /// A coordinator to act as the delegate for `UITextView`, handling text changes and other events. + /// + class Coordinator: NSObject, UITextViewDelegate { + /// The parent view. + var parent: BitwardenUITextView + + /// The calculated height of the text view. + var calculatedHeight: Binding + + /// Initializes a new `Coordinator` for the `BitwardenUITextView`. + /// + /// - Parameters: + /// - parent: The parent view that owns this coordinator. + /// - calculatedHeight: The height of the text view. + /// + init( + _ parent: BitwardenUITextView, + calculatedHeight: Binding + ) { + self.parent = parent + self.calculatedHeight = calculatedHeight + } + + func textViewDidChange(_ uiView: UITextView) { + parent.text = uiView.text + parent.recalculateHeight( + view: uiView, + result: calculatedHeight + ) + } + } + + // MARK: Properties + + /// The text entered into the text field. + @Binding var text: String + + /// The calculated height of the `UITextView`. This value is dynamically updated based on the + /// content size, and it helps to adjust the height of the view in SwiftUI. + @Binding var calculatedHeight: CGFloat + + /// Indicates whether the `UITextView` is editable. When set to `true`, the user can edit the + /// text. If `false`, the text view is read-only. + var isEditable: Bool = true + + /// Creates and returns the coordinator for the `UITextView`. + /// + /// - Returns: A `Coordinator` instance to manage the `UITextView`'s events. + /// + func makeCoordinator() -> Coordinator { + Coordinator(self, calculatedHeight: $calculatedHeight) + } + + // MARK: - UIViewRepresentable Methods + + /// Creates and configures the `UITextView` for this view. + /// + /// - Parameter context: The context containing the coordinator for this view. + /// - Returns: A configured `UITextView` instance. + /// + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.adjustsFontForContentSizeCategory = true + textView.autocapitalizationType = .sentences + textView.delegate = context.coordinator + textView.textColor = Asset.Colors.textPrimary.color + textView.isScrollEnabled = false + textView.isEditable = isEditable + textView.isUserInteractionEnabled = true + textView.isSelectable = true + textView.backgroundColor = .clear + textView.tintColor = Asset.Colors.tintPrimary.color + textView.textContainerInset = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) + textView.textContainer.lineFragmentPadding = 0 + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + let customFont = FontFamily.DMSans.regular.font(size: 15) + textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont) + return textView + } + + /// Updates the `UITextView` with the latest text when the SwiftUI state changes. + /// + /// - Parameters: + /// - uiView: The `UITextView` instance being updated. + /// - context: The context containing the coordinator for this view. + /// + func updateUIView( + _ uiView: UITextView, + context: Context + ) { + if uiView.text != text { + uiView.text = text + } + + recalculateHeight( + view: uiView, + result: $calculatedHeight + ) + } + + /// Recalculates the height of the UIView based on its content size and updates the binding if the height changes. + /// + /// - Parameters: + /// - view: The UIView whose height is to be recalculated. + /// - result: A binding to a CGFloat that stores the height value. + /// + private func recalculateHeight( + view: UIView, + result: Binding + ) { + let newSize = view.sizeThatFits( + CGSize( + width: view.frame.size.width, + height: CGFloat.greatestFiniteMagnitude + ) + ) + + if result.wrappedValue != newSize.height { + DispatchQueue.main.async { + result.wrappedValue = newSize.height + } + } + } +} diff --git a/BitwardenShared/UI/Tools/Send/Send/SendList/SendListView.swift b/BitwardenShared/UI/Tools/Send/Send/SendList/SendListView.swift index 6afdbdfdf..a143ee2e7 100644 --- a/BitwardenShared/UI/Tools/Send/Send/SendList/SendListView.swift +++ b/BitwardenShared/UI/Tools/Send/Send/SendList/SendListView.swift @@ -203,6 +203,7 @@ struct SendListView: View { placement: .navigationBarDrawer(displayMode: .always), prompt: Localizations.search ) + .autocorrectionDisabled(true) .refreshable { [weak store] in await store?.perform(.refresh) } diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift index ddbf8a687..5290de31e 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift @@ -14,6 +14,12 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length /// A state variable to track whether the TextField is focused @FocusState private var isMaxAccessCountFocused: Bool + /// The height of the notes textfield + @SwiftUI.State private var notesDynamicHeight: CGFloat = 28 + + /// The height of the text send attributes textfield + @SwiftUI.State private var textSendDynamicHeight: CGFloat = 28 + var body: some View { ZStack { ScrollView { @@ -392,14 +398,20 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length ) .textFieldConfiguration(.password) - BitwardenMultilineTextField( + BitwardenField( title: Localizations.notes, - text: store.binding( - get: \.notes, - send: AddEditSendItemAction.notesChanged - ), footer: Localizations.notesInfo - ) + ) { + BitwardenUITextView( + text: store.binding( + get: \.notes, + send: AddEditSendItemAction.notesChanged + ), + calculatedHeight: $notesDynamicHeight + ) + .frame(minHeight: notesDynamicHeight) + .accessibilityLabel(Localizations.notes) + } Toggle(Localizations.hideEmail, isOn: store.binding( get: \.isHideMyEmailOn, @@ -459,15 +471,21 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length /// The attributes for a text type send. @ViewBuilder private var textSendAttributes: some View { - BitwardenMultilineTextField( + BitwardenField( title: Localizations.text, - text: store.binding( - get: \.text, - send: AddEditSendItemAction.textChanged - ), - footer: Localizations.typeTextInfo, - accessibilityIdentifier: "SendTextContentEntry" - ) + footer: Localizations.typeTextInfo + ) { + BitwardenUITextView( + text: store.binding( + get: \.text, + send: AddEditSendItemAction.textChanged + ), + calculatedHeight: $textSendDynamicHeight + ) + .frame(minHeight: textSendDynamicHeight) + .accessibilityLabel(Localizations.text) + .accessibilityIdentifier("SendTextContentEntry") + } Toggle(Localizations.hideTextByDefault, isOn: store.binding( get: \.isHideTextByDefaultOn, diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemViewTests.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemViewTests.swift index 85abd218b..4389cf6eb 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemViewTests.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemViewTests.swift @@ -103,7 +103,10 @@ class AddEditSendItemViewTests: BitwardenTestCase { // swiftlint:disable:this ty @MainActor func test_notesTextField_updated() throws { processor.state.isOptionsExpanded = true - let textField = try subject.inspect().find(bitwardenMultilineTextField: Localizations.notes) + let textField = try subject.inspect().find( + type: BitwardenUITextViewType.self, + accessibilityLabel: Localizations.notes + ) try textField.inputBinding().wrappedValue = "Notes" XCTAssertEqual(processor.dispatchedActions.last, .notesChanged("Notes")) } @@ -163,7 +166,10 @@ class AddEditSendItemViewTests: BitwardenTestCase { // swiftlint:disable:this ty /// Updating the text textfield sends the `.textChanged` action. @MainActor func test_textTextField_updated() throws { - let textField = try subject.inspect().find(bitwardenMultilineTextField: Localizations.text) + let textField = try subject.inspect().find( + type: BitwardenUITextViewType.self, + accessibilityLabel: Localizations.text + ) try textField.inputBinding().wrappedValue = "Text" XCTAssertEqual(processor.dispatchedActions.last, .textChanged("Text")) } @@ -288,7 +294,7 @@ class AddEditSendItemViewTests: BitwardenTestCase { // swiftlint:disable:this ty func test_snapshot_text_withOptions_withValues() { processor.state.isOptionsExpanded = true processor.state.name = "Name" - processor.state.text = "Text with lots of text that wraps to new lines when displayed." + processor.state.text = "Text." processor.state.isHideTextByDefaultOn = true processor.state.deletionDate = .custom processor.state.customDeletionDate = Date(year: 2023, month: 11, day: 5, hour: 9, minute: 41) @@ -297,7 +303,7 @@ class AddEditSendItemViewTests: BitwardenTestCase { // swiftlint:disable:this ty processor.state.maximumAccessCount = 42 processor.state.maximumAccessCountText = "42" processor.state.password = "pa$$w0rd" - processor.state.notes = "Notes with lots of text that wraps to new lines when displayed." + processor.state.notes = "Notes." processor.state.isHideMyEmailOn = true processor.state.isDeactivateThisSendOn = true assertSnapshot(of: subject.navStackWrapped, as: .tallPortrait) diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_edit_withOptions_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_edit_withOptions_withValues.1.png index 69265f888..b2d35944e 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_edit_withOptions_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_edit_withOptions_withValues.1.png differ diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_withOptions_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_withOptions_withValues.1.png index b0d9ad436..e9168d899 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_withOptions_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_file_withOptions_withValues.1.png differ diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_edit_withOptions_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_edit_withOptions_withValues.1.png index 29a734386..17e62f553 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_edit_withOptions_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_edit_withOptions_withValues.1.png differ diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_extension_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_extension_withValues.1.png index 4e7332e62..5a0550b70 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_extension_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_extension_withValues.1.png differ diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withOptions_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withOptions_withValues.1.png index 856be2001..7d4a0a07c 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withOptions_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withOptions_withValues.1.png differ diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withValues.1.png b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withValues.1.png index 63dbfeb9a..dc81c747d 100644 Binary files a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withValues.1.png and b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/__Snapshots__/AddEditSendItemViewTests/test_snapshot_text_withValues.1.png differ diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift index d9684f5af..c02b3eeba 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift @@ -262,6 +262,7 @@ struct VaultListView: View { placement: .navigationBarDrawer(displayMode: .always), prompt: Localizations.search ) + .autocorrectionDisabled(true) .task(id: store.state.searchText) { await store.perform(.search(store.state.searchText)) } diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift index 2e8ad6a34..dfc9fa273 100644 --- a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift +++ b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift @@ -16,6 +16,9 @@ struct AddEditItemView: View { /// The `Store` for this view. @ObservedObject var store: Store + /// The height of the notes field + @SwiftUI.State private var notesDynamicHeight: CGFloat = 28 + /// Whether to show that a policy is in effect. var isPolicyEnabled: Bool { store.state.isPersonalOwnershipDisabled && store.state.configuration == .add @@ -250,13 +253,17 @@ private extension AddEditItemView { var notesSection: some View { SectionView(Localizations.notes) { - BitwardenMultilineTextField( - text: store.binding( - get: \.notes, - send: AddEditItemAction.notesChanged + BitwardenField { + BitwardenUITextView( + text: store.binding( + get: \.notes, + send: AddEditItemAction.notesChanged + ), + calculatedHeight: $notesDynamicHeight ) - ) - .accessibilityLabel(Localizations.notes) + .frame(minHeight: notesDynamicHeight) + .accessibilityLabel(Localizations.notes) + } } } @@ -302,12 +309,6 @@ private extension AddEditItemView { } #if DEBUG -private let multilineText = - """ - I should really keep this safe. - Is that right? - """ - struct AddEditItemView_Previews: PreviewProvider { static var cipherState: CipherItemState { var state = CipherItemState( @@ -349,6 +350,21 @@ struct AddEditItemView_Previews: PreviewProvider { } .previewDisplayName("Empty Add") + NavigationView { + AddEditItemView( + store: Store( + processor: StateProcessor( + state: { + var state = cipherState + state.notes = "This is a nice note" + return state + }() + ) + ) + ) + } + .previewDisplayName("Edit Notes") + NavigationView { AddEditItemView( store: Store( @@ -387,7 +403,6 @@ struct AddEditItemView_Previews: PreviewProvider { copy.isFavoriteOn = false copy.isMasterPasswordRePromptOn = true copy.owner = .personal(email: "security@bitwarden.com") - copy.notes = multilineText return copy.addEditState }() ) diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemViewTests.swift b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemViewTests.swift index d6a318b84..11f868e29 100644 --- a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemViewTests.swift +++ b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemViewTests.swift @@ -167,7 +167,7 @@ class AddEditItemViewTests: BitwardenTestCase { // swiftlint:disable:this type_b @MainActor func test_notesTextField_updateValue() throws { let textField = try subject.inspect().find( - type: BitwardenMultilineTextFieldType.self, + type: BitwardenUITextViewType.self, accessibilityLabel: Localizations.notes ) try textField.inputBinding().wrappedValue = "text" diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled.1.png index 1eb0d8f76..462e815f1 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled_largeText.1.png index cdaf23318..f09e92377 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_identity_full_fieldsFilled_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsNotVisible.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsNotVisible.1.png index 153c785f8..32930c1c7 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsNotVisible.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsNotVisible.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsVisible.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsVisible.1.png index c5cec9e5d..7851d5df7 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsVisible.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_login_full_fieldsVisible.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_secureNote_full_fieldsVisible.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_secureNote_full_fieldsVisible.1.png index 20ef5b896..d5c58cdd8 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_secureNote_full_fieldsVisible.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_add_secureNote_full_fieldsVisible.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_disabledViewPassword.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_disabledViewPassword.1.png index b987330c3..f439596df 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_disabledViewPassword.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_disabledViewPassword.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible.1.png index fefa144e1..4cc0c3488 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible_largeText.1.png index 49cee86e9..d39e632e2 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsNotVisible_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible.1.png index 014dc94ba..e567be264 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible_largeText.1.png index 1ed9da624..b24287fc3 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_edit_full_fieldsVisible_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.10.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.10.png index 94e75a453..776768677 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.10.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.10.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.11.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.11.png index 793076878..3f84ae232 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.11.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.11.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.12.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.12.png index 3667e57e7..bbc30bb59 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.12.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.12.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.13.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.13.png index ce3f645d5..94e75a453 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.13.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.13.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.14.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.14.png index 514df923d..26757ba26 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.14.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.14.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.15.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.15.png index 5dfed583f..3667e57e7 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.15.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.15.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.16.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.16.png new file mode 100644 index 000000000..ce3f645d5 Binary files /dev/null and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.16.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.17.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.17.png new file mode 100644 index 000000000..2e726674d Binary files /dev/null and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.17.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.18.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.18.png new file mode 100644 index 000000000..5dfed583f Binary files /dev/null and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.18.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.2.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.2.png index fd3c238f4..1fbb3a008 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.2.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.2.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.4.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.4.png index bd128f6e2..5f0b346f3 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.4.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.4.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.5.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.5.png index 9d136516d..7fbf0a59d 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.5.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.5.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.6.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.6.png index 58cfb0ff5..3667e57e7 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.6.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.6.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.7.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.7.png index 52e1c7c74..bd128f6e2 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.7.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.7.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.8.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.8.png index ed758dad6..fc20f6930 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.8.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.8.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.9.png b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.9.png index bbc30bb59..58cfb0ff5 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.9.png and b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/__Snapshots__/AddEditItemViewTests/test_snapshot_previews_addEditItemView.9.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemDetailsView.swift b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemDetailsView.swift index 9532441f0..ea775adf3 100644 --- a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemDetailsView.swift +++ b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemDetailsView.swift @@ -86,7 +86,7 @@ struct ViewItemDetailsView: View { // swiftlint:disable:this type_body_length } case .text: if let value = customField.value { - Text(value) + Text(value).textSelection(.enabled) } case .linked: if let linkedIdType = customField.linkedIdType { @@ -186,7 +186,10 @@ struct ViewItemDetailsView: View { // swiftlint:disable:this type_body_length @ViewBuilder private var notesSection: some View { if !store.state.notes.isEmpty { SectionView(Localizations.notes) { - BitwardenTextValueField(value: store.state.notes, textSelectionEnabled: true) + BitwardenTextValueField( + value: store.state.notes, + useUIKitTextView: true + ) } .accessibilityElement(children: .contain) .accessibilityIdentifier("CipherNotesLabel") diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemView.swift b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemView.swift index 04db32adc..360f8dff2 100644 --- a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemView.swift +++ b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemView.swift @@ -165,7 +165,6 @@ struct ViewItemView_Previews: PreviewProvider { state.type = CipherType.card state.isMasterPasswordRePromptOn = true state.name = "Points ALL Day" - state.notes = "Why are we so consumption focused?" state.cardItemState = CardItemState( brand: .custom(.americanExpress), cardholderName: "Bitwarden User", @@ -193,7 +192,6 @@ struct ViewItemView_Previews: PreviewProvider { ] state.isMasterPasswordRePromptOn = false state.name = "Example" - state.notes = "This is a long note so that it goes to the next line!" state.loginState.fido2Credentials = [ .fixture(creationDate: Date(timeIntervalSince1970: 1_710_494_110)), ] diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemViewTests.swift b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemViewTests.swift index 6928c7c3e..310b1cfc3 100644 --- a/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemViewTests.swift +++ b/BitwardenShared/UI/Vault/VaultItem/ViewItem/ViewItemViewTests.swift @@ -163,7 +163,7 @@ class ViewItemViewTests: BitwardenTestCase { // swiftlint:disable:this type_body )! cipherState.folderId = "1" cipherState.folders = [.custom(.fixture(id: "1", name: "Folder"))] - cipherState.notes = "This is a long note so that it goes to the next line!" + cipherState.notes = "Notes" cipherState.updatedDate = Date(year: 2023, month: 11, day: 11, hour: 9, minute: 41) cipherState.identityState = .fixture( title: .custom(.dr), @@ -205,7 +205,7 @@ class ViewItemViewTests: BitwardenTestCase { // swiftlint:disable:this type_body cipherState.folderId = "1" cipherState.folders = [.custom(.fixture(id: "1", name: "Folder"))] cipherState.name = "Example" - cipherState.notes = "This is a long note so that it goes to the next line!" + cipherState.notes = "Notes" cipherState.updatedDate = Date(year: 2023, month: 11, day: 11, hour: 9, minute: 41) cipherState.loginState.canViewPassword = canViewPassword cipherState.loginState.fido2Credentials = [.fixture()] diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues.1.png index 3f75e499b..eb623d510 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues_largeText.1.png index 6d994c924..7d6bfd6bc 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_identity_withAllValues_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_disabledViewPassword.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_disabledViewPassword.1.png index ee9f814a4..841b2f105 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_disabledViewPassword.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_disabledViewPassword.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_hiddenTotp.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_hiddenTotp.1.png index 11a110dcb..8b3a8bf87 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_hiddenTotp.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_hiddenTotp.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues.1.png index bedfd71d6..c273b6b3e 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_exceptTotp_noPremium.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_exceptTotp_noPremium.1.png index be2b76fa7..52fa0100d 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_exceptTotp_noPremium.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_exceptTotp_noPremium.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_largeText.1.png index e7b9a1bf0..6bcf22d88 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium.1.png index d12644e24..fbe2ad95c 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium_largeText.1.png index 940cf5287..22944514b 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_login_withAllValues_noPremium_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card.1.png index 4ec91f1ac..2ee5dd174 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_dark.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_dark.1.png index 030c5760d..ba2902355 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_dark.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_dark.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_largeText.1.png index 9815655d9..a753f5def 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_card_largeText.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login.1.png index 69557a06e..8c9229a44 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_dark.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_dark.1.png index 5e25a88dc..332f9724a 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_dark.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_dark.1.png differ diff --git a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_largeText.1.png b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_largeText.1.png index a0342a9f1..2b7fbc1f1 100644 Binary files a/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_largeText.1.png and b/BitwardenShared/UI/Vault/VaultItem/ViewItem/__Snapshots__/ViewItemViewTests/test_snapshot_previews_login_largeText.1.png differ diff --git a/GlobalTestHelpers/Extensions/InspectableView.swift b/GlobalTestHelpers/Extensions/InspectableView.swift index b217df890..4db8d34fc 100644 --- a/GlobalTestHelpers/Extensions/InspectableView.swift +++ b/GlobalTestHelpers/Extensions/InspectableView.swift @@ -46,15 +46,15 @@ struct BitwardenMenuFieldType: BaseViewType { ] } -/// A generic type wrapper around `BitwardenMultilineTextField` to allow `ViewInspector` to find -/// instances of `BitwardenMultilineTextField` without needing to know the details of it's +/// A generic type wrapper around `BitwardenUITextViewType` to allow `ViewInspector` to find +/// instances of `BitwardenUITextViewType` without needing to know the details of it's /// implementation. /// -struct BitwardenMultilineTextFieldType: BaseViewType { - static var typePrefix: String = "BitwardenMultilineTextField" +struct BitwardenUITextViewType: BaseViewType { + static var typePrefix: String = "BitwardenUITextView" static var namespacedPrefixes: [String] = [ - "BitwardenShared.BitwardenMultilineTextField", + "BitwardenShared.BitwardenUITextView", ] } @@ -133,21 +133,6 @@ extension InspectableView { try find(BitwardenMenuFieldType.self, containing: title, locale: locale) } - /// Attempts to locate a bitwarden multiline text field with the provided title. - /// - /// - Parameters: - /// - title: The title to use while searching for a text field. - /// - locale: The locale for text extraction. - /// - Returns: A `BitwardenMultilineTextFieldType`, if one can be located. - /// - Throws: Throws an error if a view was unable to be located. - /// - func find( - bitwardenMultilineTextField title: String, - locale: Locale = .testsDefault - ) throws -> InspectableView { - try find(BitwardenMultilineTextFieldType.self, containing: title, locale: locale) - } - /// Attempts to locate a bitwarden text field with the provided title. /// /// - Parameters: @@ -321,7 +306,7 @@ extension InspectableView where View == BitwardenTextFieldType { } } -extension InspectableView where View == BitwardenMultilineTextFieldType { +extension InspectableView where View == BitwardenUITextViewType { /// Locates the raw binding on this textfield's text value. Can be used to simulate updating the text field. /// func inputBinding() throws -> Binding { @@ -331,7 +316,7 @@ extension InspectableView where View == BitwardenMultilineTextFieldType { } else { throw InspectionError.attributeNotFound( label: "_text", - type: String(describing: BitwardenMultilineTextFieldType.self) + type: String(describing: BitwardenUITextViewType.self) ) } }