Streamline settings (#41)
@ -900,3 +900,4 @@
|
||||
"WhenUsingTwoStepVerification" = "When using 2-step verification, you’ll enter your username and password and a code generated in this app.";
|
||||
"GetStarted" = "Get started";
|
||||
"LaunchTutorial" = "Launch tutorial";
|
||||
"Help" = "Help";
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
// MARK: - AboutAction
|
||||
|
||||
/// Actions handled by the `AboutProcessor`.
|
||||
///
|
||||
enum AboutAction: Equatable {
|
||||
/// Clears the app review URL.
|
||||
case clearAppReviewURL
|
||||
|
||||
/// Clears the give feedback URL.
|
||||
case clearGiveFeedbackURL
|
||||
|
||||
/// The url has been opened so clear the value in the state.
|
||||
case clearURL
|
||||
|
||||
/// The give feedback button was tapped.
|
||||
case giveFeedbackTapped
|
||||
|
||||
/// The help center button was tapped.
|
||||
case helpCenterTapped
|
||||
|
||||
/// The learn about organizations button was tapped.
|
||||
case learnAboutOrganizationsTapped
|
||||
|
||||
/// The privacy policy button was tapped.
|
||||
case privacyPolicyTapped
|
||||
|
||||
/// The rate the app button was tapped.
|
||||
case rateTheAppTapped
|
||||
|
||||
/// The toast was shown or hidden.
|
||||
case toastShown(Toast?)
|
||||
|
||||
/// The submit crash logs toggle value changed.
|
||||
case toggleSubmitCrashLogs(Bool)
|
||||
|
||||
/// The tutorial button was tapped
|
||||
case tutorialTapped
|
||||
|
||||
/// The version was tapped.
|
||||
case versionTapped
|
||||
|
||||
/// The web vault button was tapped.
|
||||
case webVaultTapped
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
// MARK: - AboutProcessor
|
||||
|
||||
/// The processor used to manage state and handle actions for the `AboutView`.
|
||||
///
|
||||
final class AboutProcessor: StateProcessor<AboutState, AboutAction, Void> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasErrorReporter
|
||||
& HasPasteboardService
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The coordinator used to manage navigation.
|
||||
private let coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>
|
||||
|
||||
/// The services used by this processor.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes a new `AboutProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - coordinator: The coordinator used to manage navigation.
|
||||
/// - services: The services used by this processor.
|
||||
/// - state: The initial state of the processor.
|
||||
///
|
||||
init(
|
||||
coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>,
|
||||
services: Services,
|
||||
state: AboutState
|
||||
) {
|
||||
self.coordinator = coordinator
|
||||
self.services = services
|
||||
|
||||
// Set the initial value of the crash logs toggle.
|
||||
var state = state
|
||||
state.isSubmitCrashLogsToggleOn = self.services.errorReporter.isEnabled
|
||||
|
||||
super.init(state: state)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func receive(_ action: AboutAction) {
|
||||
switch action {
|
||||
case .clearAppReviewURL:
|
||||
state.appReviewUrl = nil
|
||||
case .clearGiveFeedbackURL:
|
||||
state.giveFeedbackUrl = nil
|
||||
case .clearURL:
|
||||
state.url = nil
|
||||
case .giveFeedbackTapped:
|
||||
coordinator.showAlert(.giveFeedbackAlert {
|
||||
self.state.giveFeedbackUrl = ExternalLinksConstants.giveFeedback
|
||||
})
|
||||
case .helpCenterTapped:
|
||||
state.url = ExternalLinksConstants.helpAndFeedback
|
||||
case .learnAboutOrganizationsTapped:
|
||||
coordinator.showAlert(.learnAboutOrganizationsAlert {
|
||||
self.state.url = ExternalLinksConstants.aboutOrganizations
|
||||
})
|
||||
case .privacyPolicyTapped:
|
||||
coordinator.showAlert(.privacyPolicyAlert {
|
||||
self.state.url = ExternalLinksConstants.privacyPolicy
|
||||
})
|
||||
case .rateTheAppTapped:
|
||||
coordinator.showAlert(.appStoreAlert {
|
||||
self.state.appReviewUrl = ExternalLinksConstants.appReview
|
||||
})
|
||||
case let .toastShown(newValue):
|
||||
state.toast = newValue
|
||||
case let .toggleSubmitCrashLogs(isOn):
|
||||
state.isSubmitCrashLogsToggleOn = isOn
|
||||
services.errorReporter.isEnabled = isOn
|
||||
case .tutorialTapped:
|
||||
coordinator.navigate(to: .tutorial)
|
||||
case .versionTapped:
|
||||
handleVersionTapped()
|
||||
case .webVaultTapped:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Prepare the text to be copied.
|
||||
private func handleVersionTapped() {
|
||||
// Copy the copyright text followed by the version info.
|
||||
let text = state.copyrightText + "\n\n" + state.version
|
||||
services.pasteboardService.copy(text)
|
||||
state.toast = Toast(text: Localizations.valueHasBeenCopied(Localizations.appInfo))
|
||||
}
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class AboutProcessorTests: AuthenticatorTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
|
||||
var errorReporter: MockErrorReporter!
|
||||
var pasteboardService: MockPasteboardService!
|
||||
var subject: AboutProcessor!
|
||||
|
||||
// MARK: Setup and Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
coordinator = MockCoordinator<SettingsRoute, SettingsEvent>()
|
||||
errorReporter = MockErrorReporter()
|
||||
pasteboardService = MockPasteboardService()
|
||||
|
||||
subject = AboutProcessor(
|
||||
coordinator: coordinator.asAnyCoordinator(),
|
||||
services: ServiceContainer.withMocks(
|
||||
errorReporter: errorReporter,
|
||||
pasteboardService: pasteboardService
|
||||
),
|
||||
state: AboutState()
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
coordinator = nil
|
||||
errorReporter = nil
|
||||
pasteboardService = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `init` sets the correct crash logs setting.
|
||||
func test_init_loadsValues() {
|
||||
errorReporter.isEnabled = true
|
||||
|
||||
subject = AboutProcessor(
|
||||
coordinator: coordinator.asAnyCoordinator(),
|
||||
services: ServiceContainer.withMocks(
|
||||
errorReporter: errorReporter
|
||||
),
|
||||
state: AboutState()
|
||||
)
|
||||
|
||||
XCTAssertTrue(subject.state.isSubmitCrashLogsToggleOn)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.clearAppReviewURL` clears the app review URL in the state.
|
||||
func test_receive_clearAppReviewURL() {
|
||||
subject.state.appReviewUrl = .example
|
||||
subject.receive(.clearAppReviewURL)
|
||||
XCTAssertNil(subject.state.appReviewUrl)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.clearGiveFeedbackURL` clears the URL in the state.
|
||||
func test_receive_clearGiveFeedbackURL() {
|
||||
subject.state.url = URL(string: "example.com")
|
||||
subject.receive(.clearGiveFeedbackURL)
|
||||
|
||||
XCTAssertNil(subject.state.giveFeedbackUrl)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.clearURL` clears the URL in the state.
|
||||
func test_receive_clearURL() {
|
||||
subject.state.url = .example
|
||||
subject.receive(.clearURL)
|
||||
XCTAssertNil(subject.state.url)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.giveFeedbackTapped` populates the URL in the state.
|
||||
func test_receive_giveFeedback() async throws {
|
||||
subject.receive(.giveFeedbackTapped)
|
||||
|
||||
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
||||
|
||||
// Tapping continue navigates the user to the web app.
|
||||
try await alert.tapAction(title: Localizations.continue)
|
||||
XCTAssertNotNil(subject.state.giveFeedbackUrl)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.helpCenterTapped` set the URL to open in the state.
|
||||
func test_receive_helpCenterTapped() {
|
||||
subject.receive(.helpCenterTapped)
|
||||
XCTAssertEqual(subject.state.url, ExternalLinksConstants.helpAndFeedback)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.learnAboutOrganizationsTapped` shows an alert for navigating to the website
|
||||
/// When `Continue` is tapped on the alert, sets the URL to open in the state.
|
||||
func test_receive_learnAboutOrganizationsTapped() async throws {
|
||||
subject.receive(.learnAboutOrganizationsTapped)
|
||||
|
||||
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
||||
try await alert.tapAction(title: Localizations.continue)
|
||||
XCTAssertEqual(subject.state.url, ExternalLinksConstants.aboutOrganizations)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.privacyPolicyTapped` shows an alert for navigating to the Privacy Policy
|
||||
/// When `Continue` is tapped on the alert, sets the URL to open in the state.
|
||||
func test_receive_privacyPolicyTapped() async throws {
|
||||
subject.receive(.privacyPolicyTapped)
|
||||
|
||||
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
||||
try await alert.tapAction(title: Localizations.continue)
|
||||
XCTAssertEqual(subject.state.url, ExternalLinksConstants.privacyPolicy)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.rateTheAppTapped` shows an alert for navigating to the app store.
|
||||
/// When `Continue` is tapped on the alert, the `appReviewUrl` is populated.
|
||||
func test_receive_rateTheAppTapped() async throws {
|
||||
subject.receive(.rateTheAppTapped)
|
||||
|
||||
let alert = try XCTUnwrap(coordinator.alertShown.last)
|
||||
try await alert.tapAction(title: Localizations.continue)
|
||||
XCTAssertEqual(
|
||||
subject.state.appReviewUrl?.absoluteString,
|
||||
"https://itunes.apple.com/us/app/id1137397744?action=write-review"
|
||||
)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.toastShown` updates the state's toast value.
|
||||
func test_receive_toastShown() {
|
||||
let toast = Toast(text: "toast!")
|
||||
subject.receive(.toastShown(toast))
|
||||
XCTAssertEqual(subject.state.toast, toast)
|
||||
|
||||
subject.receive(.toastShown(nil))
|
||||
XCTAssertNil(subject.state.toast)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with action `.isSubmitCrashLogsToggleOn` updates the toggle value in the state.
|
||||
func test_receive_toggleSubmitCrashLogs() {
|
||||
errorReporter.isEnabled = false
|
||||
XCTAssertFalse(subject.state.isSubmitCrashLogsToggleOn)
|
||||
|
||||
subject.receive(.toggleSubmitCrashLogs(true))
|
||||
|
||||
XCTAssertTrue(subject.state.isSubmitCrashLogsToggleOn)
|
||||
XCTAssertTrue(errorReporter.isEnabled)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with action `.versionTapped` copies the version string to the pasteboard.
|
||||
func test_receive_versionTapped() {
|
||||
subject.receive(.versionTapped)
|
||||
let text = subject.state.copyrightText + "\n\n" + subject.state.version
|
||||
XCTAssertEqual(pasteboardService.copiedString, text)
|
||||
XCTAssertEqual(subject.state.toast?.text, Localizations.valueHasBeenCopied(Localizations.appInfo))
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - AboutState
|
||||
|
||||
/// An object that defines the current state of the `AboutView`.
|
||||
///
|
||||
struct AboutState {
|
||||
/// The URL for Bitwarden's app review page in the app store.
|
||||
var appReviewUrl: URL?
|
||||
|
||||
/// The copyright text.
|
||||
var copyrightText = "© Bitwarden Inc. 2015-\(Calendar.current.component(.year, from: Date.now))"
|
||||
|
||||
/// The URL of the feedback webpage.
|
||||
var giveFeedbackUrl: URL?
|
||||
|
||||
/// Whether the submit crash logs toggle is on.
|
||||
var isSubmitCrashLogsToggleOn: Bool = false
|
||||
|
||||
/// A toast message to show in the view.
|
||||
var toast: Toast?
|
||||
|
||||
/// The url to open in the device's web browser.
|
||||
var url: URL?
|
||||
|
||||
/// The version of the app.
|
||||
var version: String = "\(Localizations.version): \(Bundle.main.appVersion) (\(Bundle.main.buildNumber))"
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - AboutView
|
||||
|
||||
/// A view that allows users to view miscellaneous information about the app.
|
||||
///
|
||||
struct AboutView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// An object used to open urls from this view.
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
/// The `Store` for this view.
|
||||
@ObservedObject var store: Store<AboutState, AboutAction, Void>
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
submitCrashLogs
|
||||
|
||||
miscSection
|
||||
|
||||
copyrightNotice
|
||||
}
|
||||
.scrollView()
|
||||
.navigationBar(title: Localizations.about, titleDisplayMode: .inline)
|
||||
.toast(store.binding(
|
||||
get: \.toast,
|
||||
send: AboutAction.toastShown
|
||||
))
|
||||
.onChange(of: store.state.url) { newValue in
|
||||
guard let url = newValue else { return }
|
||||
openURL(url)
|
||||
store.send(.clearURL)
|
||||
}
|
||||
.onChange(of: store.state.appReviewUrl) { newValue in
|
||||
guard let url = newValue else { return }
|
||||
openURL(url)
|
||||
store.send(.clearAppReviewURL)
|
||||
}
|
||||
.onChange(of: store.state.giveFeedbackUrl) { newValue in
|
||||
guard let url = newValue else { return }
|
||||
openURL(url)
|
||||
store.send(.clearGiveFeedbackURL)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private views
|
||||
|
||||
/// The copyright notice.
|
||||
private var copyrightNotice: some View {
|
||||
Text(store.state.copyrightText)
|
||||
.styleGuide(.caption2)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
/// The section of miscellaneous about items.
|
||||
private var miscSection: some View {
|
||||
VStack(spacing: 0) {
|
||||
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped)
|
||||
|
||||
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
|
||||
|
||||
externalLinkRow(Localizations.webVault, action: .webVaultTapped)
|
||||
|
||||
externalLinkRow(Localizations.learnOrg, action: .learnAboutOrganizationsTapped)
|
||||
|
||||
externalLinkRow(Localizations.giveFeedback, action: .giveFeedbackTapped)
|
||||
|
||||
SettingsListItem(Localizations.launchTutorial) {
|
||||
store.send(.tutorialTapped)
|
||||
}
|
||||
|
||||
SettingsListItem(store.state.version, hasDivider: false) {
|
||||
store.send(.versionTapped)
|
||||
} trailingContent: {
|
||||
Asset.Images.copy.swiftUIImage
|
||||
.imageStyle(.rowIcon)
|
||||
}
|
||||
}
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
/// The submit crash logs toggle.
|
||||
private var submitCrashLogs: some View {
|
||||
Toggle(isOn: store.binding(
|
||||
get: \.isSubmitCrashLogsToggleOn,
|
||||
send: AboutAction.toggleSubmitCrashLogs
|
||||
)) {
|
||||
Text(Localizations.submitCrashLogs)
|
||||
}
|
||||
.toggleStyle(.bitwarden)
|
||||
.styleGuide(.body)
|
||||
.accessibilityIdentifier("SubmitCrashLogsSwitch")
|
||||
}
|
||||
|
||||
/// Returns a `SettingsListItem` configured for an external web link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The localized name of the row.
|
||||
/// - action: An action to send when the row is tapped.
|
||||
/// - Returns: A `SettingsListItem` configured for an external web link.
|
||||
///
|
||||
private func externalLinkRow(_ name: String, action: AboutAction) -> some View {
|
||||
SettingsListItem(name) {
|
||||
store.send(action)
|
||||
} trailingContent: {
|
||||
Asset.Images.externalLink2.swiftUIImage
|
||||
.imageStyle(.rowIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
AboutView(store: Store(processor: StateProcessor(state: AboutState())))
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class AboutViewTests: AuthenticatorTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
let copyrightText = "© Bitwarden Inc. 2015-2023"
|
||||
let version = "Version: 1.0.0 (1)"
|
||||
|
||||
var processor: MockProcessor<AboutState, AboutAction, Void>!
|
||||
var subject: AboutView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(state: AboutState(copyrightText: copyrightText, version: version))
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = AboutView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Tapping the give feedback button dispatches the `.giveFeedbackTapped` action.
|
||||
func test_giveFeedbackButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.giveFeedback)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .giveFeedbackTapped)
|
||||
}
|
||||
|
||||
/// Tapping the help center button dispatches the `.helpCenterTapped` action.
|
||||
func test_helpCenterButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.bitwardenHelpCenter)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .helpCenterTapped)
|
||||
}
|
||||
|
||||
/// Tapping the privacy policy button dispatches the `.privacyPolicyTapped` action.
|
||||
func test_privacyPolicyButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.privacyPolicy)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .privacyPolicyTapped)
|
||||
}
|
||||
|
||||
/// Tapping the learn about organizations button dispatches the `.learnAboutOrganizationsTapped` action.
|
||||
func test_learnAboutOrganizationsButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.learnOrg)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .learnAboutOrganizationsTapped)
|
||||
}
|
||||
|
||||
/// Tapping the version button dispatches the `.versionTapped` action.
|
||||
func test_versionButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: version)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .versionTapped)
|
||||
}
|
||||
|
||||
/// Tapping the web vault button dispatches the `.webVaultTapped` action.
|
||||
func test_webVaultButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.webVault)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .webVaultTapped)
|
||||
}
|
||||
|
||||
// MARK: Snapshots
|
||||
|
||||
/// The default view renders correctly.
|
||||
func test_snapshot_default() {
|
||||
assertSnapshots(of: subject, as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5])
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 193 KiB |
@ -1,14 +0,0 @@
|
||||
// MARK: - AppearanceAction
|
||||
|
||||
/// Actions handled by the `AppearanceProcessor`.
|
||||
///
|
||||
enum AppearanceAction: Equatable {
|
||||
/// The default color theme was changed.
|
||||
case appThemeChanged(AppTheme)
|
||||
|
||||
/// The language option was tapped.
|
||||
case languageTapped
|
||||
|
||||
/// Show website icons was toggled.
|
||||
case toggleShowWebsiteIcons(Bool)
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
// MARK: - AppearanceEffect
|
||||
|
||||
/// Effects that can be processed by an `AppearanceProcessor`.
|
||||
enum AppearanceEffect {
|
||||
/// The view appeared so the initial data should be loaded.
|
||||
case loadData
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - AppearanceProcessor
|
||||
|
||||
/// The processor used to manage state and handle actions for the `AppearanceView`.
|
||||
///
|
||||
final class AppearanceProcessor: StateProcessor<AppearanceState, AppearanceAction, AppearanceEffect> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasStateService
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The `Coordinator` that handles navigation.
|
||||
private let coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>
|
||||
|
||||
/// The services for this processor.
|
||||
private var services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes a new `AppearanceProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - coordinator: The `Coordinator` that handles navigation.
|
||||
/// - services: The services for this processor.
|
||||
/// - state: The initial state of the processor.
|
||||
///
|
||||
init(
|
||||
coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>,
|
||||
services: Services,
|
||||
state: AppearanceState
|
||||
) {
|
||||
self.coordinator = coordinator
|
||||
self.services = services
|
||||
|
||||
super.init(state: state)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func perform(_ effect: AppearanceEffect) async {
|
||||
switch effect {
|
||||
case .loadData:
|
||||
state.currentLanguage = services.stateService.appLanguage
|
||||
state.appTheme = await services.stateService.getAppTheme()
|
||||
state.isShowWebsiteIconsToggleOn = await services.stateService.getShowWebIcons()
|
||||
}
|
||||
}
|
||||
|
||||
override func receive(_ action: AppearanceAction) {
|
||||
switch action {
|
||||
case let .appThemeChanged(appTheme):
|
||||
state.appTheme = appTheme
|
||||
Task {
|
||||
await services.stateService.setAppTheme(appTheme)
|
||||
}
|
||||
case .languageTapped:
|
||||
coordinator.navigate(to: .selectLanguage(currentLanguage: state.currentLanguage), context: self)
|
||||
case let .toggleShowWebsiteIcons(isOn):
|
||||
state.isShowWebsiteIconsToggleOn = isOn
|
||||
Task {
|
||||
await services.stateService.setShowWebIcons(isOn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SelectLanguageDelegate
|
||||
|
||||
extension AppearanceProcessor: SelectLanguageDelegate {
|
||||
/// Update the language selection.
|
||||
func languageSelected(_ languageOption: LanguageOption) {
|
||||
state.currentLanguage = languageOption
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class AppearanceProcessorTests: AuthenticatorTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var coordinator: MockCoordinator<SettingsRoute, SettingsEvent>!
|
||||
var stateService: MockStateService!
|
||||
var subject: AppearanceProcessor!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
coordinator = MockCoordinator()
|
||||
stateService = MockStateService()
|
||||
let services = ServiceContainer.withMocks(
|
||||
stateService: stateService
|
||||
)
|
||||
|
||||
subject = AppearanceProcessor(
|
||||
coordinator: coordinator.asAnyCoordinator(),
|
||||
services: services,
|
||||
state: AppearanceState()
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
coordinator = nil
|
||||
stateService = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// The delegate method `languageSelected` should update the language.
|
||||
func test_languageSelected() {
|
||||
XCTAssertEqual(subject.state.currentLanguage, .default)
|
||||
|
||||
subject.languageSelected(.custom(languageCode: "th"))
|
||||
|
||||
XCTAssertEqual(subject.state.currentLanguage, .custom(languageCode: "th"))
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.loadData` sets the value in the state.
|
||||
func test_perform_loadData() async {
|
||||
XCTAssertEqual(subject.state.appTheme, .default)
|
||||
stateService.appLanguage = .custom(languageCode: "de")
|
||||
stateService.appTheme = .light
|
||||
stateService.showWebIcons = false
|
||||
|
||||
await subject.perform(.loadData)
|
||||
|
||||
XCTAssertEqual(subject.state.currentLanguage, .custom(languageCode: "de"))
|
||||
XCTAssertEqual(subject.state.appTheme, .light)
|
||||
XCTAssertFalse(subject.state.isShowWebsiteIconsToggleOn)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.appThemeChanged` updates the theme.
|
||||
func test_receive_appThemeChanged() {
|
||||
subject.receive(.appThemeChanged(.dark))
|
||||
|
||||
XCTAssertEqual(subject.state.appTheme, .dark)
|
||||
waitFor(stateService.appTheme == .dark)
|
||||
|
||||
subject.receive(.appThemeChanged(.light))
|
||||
|
||||
XCTAssertEqual(subject.state.appTheme, .light)
|
||||
waitFor(stateService.appTheme == .light)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.languageTapped` navigates to the select language view.
|
||||
func test_receive_languageTapped() async throws {
|
||||
subject.state.currentLanguage = .custom(languageCode: "th")
|
||||
|
||||
subject.receive(.languageTapped)
|
||||
|
||||
XCTAssertEqual(coordinator.routes.last, .selectLanguage(currentLanguage: LanguageOption("th")))
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.toggleShowWebsiteIcons` updates the value in the state and the cache.
|
||||
func test_receive_toggleShowWebsiteIcons() {
|
||||
XCTAssertFalse(subject.state.isShowWebsiteIconsToggleOn)
|
||||
|
||||
subject.receive(.toggleShowWebsiteIcons(true))
|
||||
|
||||
XCTAssertTrue(subject.state.isShowWebsiteIconsToggleOn)
|
||||
waitFor(stateService.showWebIcons == true)
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// MARK: - AppearanceState
|
||||
|
||||
/// An object that defines the current state of the `AppearanceView`.
|
||||
///
|
||||
struct AppearanceState {
|
||||
/// The selected app theme.
|
||||
var appTheme: AppTheme = .default
|
||||
|
||||
/// Whether or not the show website icons toggle is on.
|
||||
var isShowWebsiteIconsToggleOn: Bool = false
|
||||
|
||||
/// The current language selection.
|
||||
var currentLanguage: LanguageOption = .default
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - AppearanceView
|
||||
|
||||
/// A view for configuring appearance settings.
|
||||
///
|
||||
struct AppearanceView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The store used to render the view.
|
||||
@ObservedObject var store: Store<AppearanceState, AppearanceAction, AppearanceEffect>
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
language
|
||||
|
||||
theme
|
||||
}
|
||||
.scrollView()
|
||||
.navigationBar(title: Localizations.appearance, titleDisplayMode: .inline)
|
||||
.task {
|
||||
await store.perform(.loadData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private views
|
||||
|
||||
/// The language picker view
|
||||
private var language: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SettingsListItem(
|
||||
Localizations.language,
|
||||
hasDivider: false
|
||||
) {
|
||||
store.send(.languageTapped)
|
||||
} trailingContent: {
|
||||
Text(store.state.currentLanguage.title)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
|
||||
Text(Localizations.languageChangeRequiresAppRestart)
|
||||
.styleGuide(.subheadline)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
}
|
||||
|
||||
/// The application's color theme picker view
|
||||
private var theme: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SettingsMenuField(
|
||||
title: Localizations.theme,
|
||||
options: AppTheme.allCases,
|
||||
hasDivider: false,
|
||||
selection: store.binding(
|
||||
get: \.appTheme,
|
||||
send: AppearanceAction.appThemeChanged
|
||||
)
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.accessibilityIdentifier("ThemeChooser")
|
||||
|
||||
Text(Localizations.themeDescription)
|
||||
.styleGuide(.subheadline)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
}
|
||||
|
||||
/// The show website icons toggle.
|
||||
private var webSiteIconsToggle: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Toggle(isOn: store.binding(
|
||||
get: \.isShowWebsiteIconsToggleOn,
|
||||
send: AppearanceAction.toggleShowWebsiteIcons
|
||||
)) {
|
||||
Text(Localizations.showWebsiteIcons)
|
||||
}
|
||||
.toggleStyle(.bitwarden)
|
||||
.styleGuide(.body)
|
||||
.accessibilityIdentifier("ShowWebsiteIconsSwitch")
|
||||
|
||||
Text(Localizations.showWebsiteIconsDescription)
|
||||
.styleGuide(.subheadline)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Previews
|
||||
|
||||
#Preview {
|
||||
AppearanceView(store: Store(processor: StateProcessor(state: AppearanceState())))
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
// MARK: - AppearanceViewTests
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class AppearanceViewTests: AuthenticatorTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<AppearanceState, AppearanceAction, AppearanceEffect>!
|
||||
var subject: AppearanceView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(state: AppearanceState())
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = AppearanceView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Updating the value of the app theme sends the `.appThemeChanged()` action.
|
||||
func test_appThemeChanged_updateValue() throws {
|
||||
processor.state.appTheme = .light
|
||||
let menuField = try subject.inspect().find(settingsMenuField: Localizations.theme)
|
||||
try menuField.select(newValue: AppTheme.dark)
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .appThemeChanged(.dark))
|
||||
}
|
||||
|
||||
/// Tapping the language button dispatches the `.languageTapped` action.
|
||||
func test_languageButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.language)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .languageTapped)
|
||||
}
|
||||
|
||||
// MARK: Snapshots
|
||||
|
||||
/// Tests the view renders correctly.
|
||||
func test_viewRender() {
|
||||
assertSnapshots(
|
||||
of: subject,
|
||||
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 654 KiB |
@ -1,9 +1,27 @@
|
||||
/// Actions that can be processed by a `SettingsProcessor`.
|
||||
///
|
||||
enum SettingsAction: Equatable {
|
||||
/// The about button was pressed.
|
||||
case aboutPressed
|
||||
/// The default color theme was changed.
|
||||
case appThemeChanged(AppTheme)
|
||||
|
||||
/// The appearance button was pressed.
|
||||
case appearancePressed
|
||||
/// The url has been opened so clear the value in the state.
|
||||
case clearURL
|
||||
|
||||
/// The help center button was tapped.
|
||||
case helpCenterTapped
|
||||
|
||||
/// The language option was tapped.
|
||||
case languageTapped
|
||||
|
||||
/// The privacy policy button was tapped.
|
||||
case privacyPolicyTapped
|
||||
|
||||
/// The toast was shown or hidden.
|
||||
case toastShown(Toast?)
|
||||
|
||||
/// The tutorial button was tapped
|
||||
case tutorialTapped
|
||||
|
||||
/// The version was tapped.
|
||||
case versionTapped
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
// MARK: - SettingsEffect
|
||||
|
||||
/// Effects that can be processed by an `SettingsProcessor`.
|
||||
enum SettingsEffect {
|
||||
/// The view appeared so the initial data should be loaded.
|
||||
case loadData
|
||||
}
|
||||
@ -2,40 +2,93 @@
|
||||
|
||||
/// The processor used to manage state and handle actions for the settings screen.
|
||||
///
|
||||
final class SettingsProcessor: StateProcessor<SettingsState, SettingsAction, Void> {
|
||||
final class SettingsProcessor: StateProcessor<SettingsState, SettingsAction, SettingsEffect> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasErrorReporter
|
||||
& HasPasteboardService
|
||||
& HasStateService
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The `Coordinator` that handles navigation.
|
||||
private let coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>
|
||||
|
||||
/// The services for this processor.
|
||||
private var services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `SettingsProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - coordinator: The `Coordinator` that handles navigation.
|
||||
/// - services: The services for this processor.
|
||||
/// - state: The initial state of the processor.
|
||||
///
|
||||
init(
|
||||
coordinator: AnyCoordinator<SettingsRoute, SettingsEvent>,
|
||||
services: Services,
|
||||
state: SettingsState
|
||||
) {
|
||||
self.coordinator = coordinator
|
||||
self.services = services
|
||||
super.init(state: state)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func receive(_ action: SettingsAction) {
|
||||
switch action {
|
||||
case .aboutPressed:
|
||||
coordinator.navigate(to: .about)
|
||||
case .appearancePressed:
|
||||
coordinator.navigate(to: .appearance)
|
||||
override func perform(_ effect: SettingsEffect) async {
|
||||
switch effect {
|
||||
case .loadData:
|
||||
state.currentLanguage = services.stateService.appLanguage
|
||||
state.appTheme = await services.stateService.getAppTheme()
|
||||
}
|
||||
}
|
||||
|
||||
override func receive(_ action: SettingsAction) {
|
||||
switch action {
|
||||
case let .appThemeChanged(appTheme):
|
||||
state.appTheme = appTheme
|
||||
Task {
|
||||
await services.stateService.setAppTheme(appTheme)
|
||||
}
|
||||
case .clearURL:
|
||||
state.url = nil
|
||||
case .helpCenterTapped:
|
||||
state.url = ExternalLinksConstants.helpAndFeedback
|
||||
case .languageTapped:
|
||||
coordinator.navigate(to: .selectLanguage(currentLanguage: state.currentLanguage), context: self)
|
||||
case .privacyPolicyTapped:
|
||||
coordinator.showAlert(.privacyPolicyAlert {
|
||||
self.state.url = ExternalLinksConstants.privacyPolicy
|
||||
})
|
||||
|
||||
case let .toastShown(newValue):
|
||||
state.toast = newValue
|
||||
case .tutorialTapped:
|
||||
coordinator.navigate(to: .tutorial)
|
||||
case .versionTapped:
|
||||
handleVersionTapped()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Prepare the text to be copied.
|
||||
private func handleVersionTapped() {
|
||||
// Copy the copyright text followed by the version info.
|
||||
let text = "Bitwarden Authenticator\n\n" + state.copyrightText + "\n\n" + state.version
|
||||
services.pasteboardService.copy(text)
|
||||
state.toast = Toast(text: Localizations.valueHasBeenCopied(Localizations.appInfo))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SelectLanguageDelegate
|
||||
|
||||
extension SettingsProcessor: SelectLanguageDelegate {
|
||||
/// Update the language selection.
|
||||
func languageSelected(_ languageOption: LanguageOption) {
|
||||
state.currentLanguage = languageOption
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
/// An object that defines the current state of a `SettingsView`.
|
||||
///
|
||||
struct SettingsState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The selected app theme.
|
||||
var appTheme: AppTheme = .default
|
||||
|
||||
/// The copyright text.
|
||||
var copyrightText = "© Bitwarden Inc. 2015-\(Calendar.current.component(.year, from: Date.now))"
|
||||
|
||||
/// The current language selection.
|
||||
var currentLanguage: LanguageOption = .default
|
||||
|
||||
/// A toast message to show in the view.
|
||||
var toast: Toast?
|
||||
|
||||
/// The url to open in the device's web browser.
|
||||
var url: URL?
|
||||
|
||||
/// The version of the app.
|
||||
var version: String = "\(Localizations.version): \(Bundle.main.appVersion) (\(Bundle.main.buildNumber))"
|
||||
}
|
||||
|
||||
@ -7,8 +7,11 @@ import SwiftUI
|
||||
struct SettingsView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// An object used to open urls from this view.
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
/// The `Store` for this view.
|
||||
@ObservedObject var store: Store<SettingsState, SettingsAction, Void>
|
||||
@ObservedObject var store: Store<SettingsState, SettingsAction, SettingsEffect>
|
||||
|
||||
// MARK: View
|
||||
|
||||
@ -16,6 +19,18 @@ struct SettingsView: View {
|
||||
settingsItems
|
||||
.scrollView()
|
||||
.navigationBar(title: Localizations.settings, titleDisplayMode: .large)
|
||||
.toast(store.binding(
|
||||
get: \.toast,
|
||||
send: SettingsAction.toastShown
|
||||
))
|
||||
.onChange(of: store.state.url) { newValue in
|
||||
guard let url = newValue else { return }
|
||||
openURL(url)
|
||||
store.send(.clearURL)
|
||||
}
|
||||
.task {
|
||||
await store.perform(.loadData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private views
|
||||
@ -28,25 +43,109 @@ struct SettingsView: View {
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
|
||||
/// The copyright notice.
|
||||
private var copyrightNotice: some View {
|
||||
Text(store.state.copyrightText)
|
||||
.styleGuide(.caption2)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
/// The language picker view
|
||||
private var language: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SettingsListItem(
|
||||
Localizations.language,
|
||||
hasDivider: false
|
||||
) {
|
||||
store.send(.languageTapped)
|
||||
} trailingContent: {
|
||||
Text(store.state.currentLanguage.title)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
|
||||
Text(Localizations.languageChangeRequiresAppRestart)
|
||||
.styleGuide(.subheadline)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings items.
|
||||
private var settingsItems: some View {
|
||||
VStack(spacing: 0) {
|
||||
SettingsListItem(Localizations.appearance) {
|
||||
store.send(.appearancePressed)
|
||||
} trailingContent: {
|
||||
chevron
|
||||
SectionView(Localizations.appearance) {
|
||||
language
|
||||
theme
|
||||
}
|
||||
.accessibilityIdentifier("AppearanceSettingsButton")
|
||||
.padding(.bottom, 32)
|
||||
|
||||
SettingsListItem(Localizations.about, hasDivider: false) {
|
||||
store.send(.aboutPressed)
|
||||
} trailingContent: {
|
||||
chevron
|
||||
SectionView(Localizations.help, contentSpacing: 0) {
|
||||
SettingsListItem(Localizations.launchTutorial) {
|
||||
store.send(.tutorialTapped)
|
||||
}
|
||||
|
||||
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped, hasDivider: false)
|
||||
}
|
||||
.accessibilityIdentifier("AboutSettingsButton")
|
||||
.padding(.bottom, 32)
|
||||
|
||||
SectionView(Localizations.about, contentSpacing: 0) {
|
||||
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
|
||||
|
||||
SettingsListItem(store.state.version, hasDivider: false) {
|
||||
store.send(.versionTapped)
|
||||
} trailingContent: {
|
||||
Asset.Images.copy.swiftUIImage
|
||||
.imageStyle(.rowIcon)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
|
||||
copyrightNotice
|
||||
}
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
/// The application's color theme picker view
|
||||
private var theme: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SettingsMenuField(
|
||||
title: Localizations.theme,
|
||||
options: AppTheme.allCases,
|
||||
hasDivider: false,
|
||||
selection: store.binding(
|
||||
get: \.appTheme,
|
||||
send: SettingsAction.appThemeChanged
|
||||
)
|
||||
)
|
||||
.cornerRadius(10)
|
||||
.accessibilityIdentifier("ThemeChooser")
|
||||
|
||||
Text(Localizations.themeDescription)
|
||||
.styleGuide(.subheadline)
|
||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `SettingsListItem` configured for an external web link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The localized name of the row.
|
||||
/// - action: An action to send when the row is tapped.
|
||||
/// - Returns: A `SettingsListItem` configured for an external web link.
|
||||
///
|
||||
private func externalLinkRow(
|
||||
_ name: String,
|
||||
action: SettingsAction,
|
||||
hasDivider: Bool = true
|
||||
) -> some View {
|
||||
SettingsListItem(name, hasDivider: hasDivider) {
|
||||
store.send(action)
|
||||
} trailingContent: {
|
||||
Asset.Images.externalLink2.swiftUIImage
|
||||
.imageStyle(.rowIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@ -55,10 +55,6 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
|
||||
func navigate(to route: SettingsRoute, context: AnyObject?) {
|
||||
switch route {
|
||||
case .about:
|
||||
showAbout()
|
||||
case .appearance:
|
||||
showAppearance()
|
||||
case .dismiss:
|
||||
stackNavigator?.dismiss()
|
||||
case let .selectLanguage(currentLanguage: currentLanguage):
|
||||
@ -76,36 +72,6 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// Shows the about screen.
|
||||
///
|
||||
private func showAbout() {
|
||||
let processor = AboutProcessor(
|
||||
coordinator: asAnyCoordinator(),
|
||||
services: services,
|
||||
state: AboutState()
|
||||
)
|
||||
|
||||
let view = AboutView(store: Store(processor: processor))
|
||||
let viewController = UIHostingController(rootView: view)
|
||||
viewController.navigationItem.largeTitleDisplayMode = .never
|
||||
stackNavigator?.push(viewController, navigationTitle: Localizations.about)
|
||||
}
|
||||
|
||||
/// Shows the appearance screen.
|
||||
///
|
||||
private func showAppearance() {
|
||||
let processor = AppearanceProcessor(
|
||||
coordinator: asAnyCoordinator(),
|
||||
services: services,
|
||||
state: AppearanceState()
|
||||
)
|
||||
|
||||
let view = AppearanceView(store: Store(processor: processor))
|
||||
let viewController = UIHostingController(rootView: view)
|
||||
viewController.navigationItem.largeTitleDisplayMode = .never
|
||||
stackNavigator?.push(viewController, navigationTitle: Localizations.appearance)
|
||||
}
|
||||
|
||||
/// Shows the select language screen.
|
||||
///
|
||||
private func showSelectLanguage(currentLanguage: LanguageOption, delegate: SelectLanguageDelegate?) {
|
||||
@ -125,6 +91,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
private func showSettings() {
|
||||
let processor = SettingsProcessor(
|
||||
coordinator: asAnyCoordinator(),
|
||||
services: services,
|
||||
state: SettingsState()
|
||||
)
|
||||
let view = SettingsView(store: Store(processor: processor))
|
||||
|
||||
@ -4,12 +4,6 @@ import Foundation
|
||||
/// A route to a specific screen in the settings tab.
|
||||
///
|
||||
public enum SettingsRoute: Equatable, Hashable {
|
||||
/// A route to the about view.
|
||||
case about
|
||||
|
||||
/// A route to the appearance screen.
|
||||
case appearance
|
||||
|
||||
/// A route that dismisses the current view.
|
||||
case dismiss
|
||||
|
||||
|
||||