mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 00:42:29 -06:00
[PM-27246] Update Authenticator to use ActionCard (#2123)
This commit is contained in:
parent
0bebf577b4
commit
0e2991802d
@ -1,27 +0,0 @@
|
|||||||
// swiftlint:disable:this file_name
|
|
||||||
import BitwardenResources
|
|
||||||
import SnapshotTesting
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@testable import AuthenticatorShared
|
|
||||||
|
|
||||||
// MARK: - ItemListCardViewTests
|
|
||||||
|
|
||||||
class ItemListCardViewTests: BitwardenTestCase {
|
|
||||||
// MARK: Tests
|
|
||||||
|
|
||||||
/// Test a snapshot of the ItemListView previews.
|
|
||||||
func disabletest_snapshot_ItemListCardView_previews() {
|
|
||||||
for preview in ItemListCardView_Previews._allPreviews {
|
|
||||||
let name = preview.displayName ?? "Unknown"
|
|
||||||
assertSnapshots(
|
|
||||||
of: preview.content,
|
|
||||||
as: [
|
|
||||||
"\(name)-portrait": .defaultPortrait,
|
|
||||||
"\(name)-portraitDark": .defaultPortraitDark,
|
|
||||||
"\(name)-portraitAX5": .tallPortraitAX5(heightMultiple: 3),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
// swiftlint:disable:this file_name
|
|
||||||
import BitwardenResources
|
|
||||||
import ViewInspector
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@testable import AuthenticatorShared
|
|
||||||
|
|
||||||
// MARK: - ItemListCardViewTests
|
|
||||||
|
|
||||||
class ItemListCardViewTests: BitwardenTestCase {
|
|
||||||
// MARK: Tests
|
|
||||||
|
|
||||||
/// Test the actions are properly wired up in the ItemListCardView.
|
|
||||||
func test_ItemListCardView_actions() throws {
|
|
||||||
let expectationAction = expectation(description: "action Tapped")
|
|
||||||
let expectationClose = expectation(description: "close Tapped")
|
|
||||||
let subject = ItemListCardView(
|
|
||||||
bodyText: Localizations
|
|
||||||
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
|
|
||||||
buttonText: Localizations.takeMeToTheAppSettings,
|
|
||||||
leftImage: {},
|
|
||||||
titleText: Localizations.syncWithTheBitwardenApp,
|
|
||||||
actionTapped: {
|
|
||||||
expectationAction.fulfill()
|
|
||||||
},
|
|
||||||
closeTapped: {
|
|
||||||
expectationClose.fulfill()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap()
|
|
||||||
wait(for: [expectationClose])
|
|
||||||
|
|
||||||
try subject.inspect().find(button: Localizations.takeMeToTheAppSettings).tap()
|
|
||||||
wait(for: [expectationAction])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import BitwardenResources
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - ItemListCardView
|
|
||||||
|
|
||||||
/// An item list card view,
|
|
||||||
///
|
|
||||||
struct ItemListCardView<ImageContent: View>: View {
|
|
||||||
// MARK: Properties
|
|
||||||
|
|
||||||
/// The body text to display in the card.
|
|
||||||
var bodyText: String
|
|
||||||
|
|
||||||
/// The button text to display in the card.
|
|
||||||
var buttonText: String
|
|
||||||
|
|
||||||
/// The image to display in the card.
|
|
||||||
@ViewBuilder let leftImage: ImageContent
|
|
||||||
|
|
||||||
/// The button text for the secondary button in the card.
|
|
||||||
var secondaryButtonText: String?
|
|
||||||
|
|
||||||
/// The title text to display in the card.
|
|
||||||
var titleText: String
|
|
||||||
|
|
||||||
// MARK: Closures
|
|
||||||
|
|
||||||
/// The callback action to perform.
|
|
||||||
var actionTapped: () -> Void
|
|
||||||
|
|
||||||
/// The close callback to perform.
|
|
||||||
var closeTapped: () -> Void
|
|
||||||
|
|
||||||
/// The action to perform when the secondary button is tapped.
|
|
||||||
var secondaryActionTapped: (() -> Void)?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
HStack(alignment: .top, spacing: 16) {
|
|
||||||
leftImage
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Group {
|
|
||||||
Text(titleText)
|
|
||||||
.styleGuide(.headline)
|
|
||||||
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
|
|
||||||
|
|
||||||
Text(bodyText)
|
|
||||||
.styleGuide(.subheadline)
|
|
||||||
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
closeTapped()
|
|
||||||
} label: {
|
|
||||||
Image(decorative: SharedAsset.Icons.close16)
|
|
||||||
.padding(16) // Add padding to increase tappable area...
|
|
||||||
}
|
|
||||||
.padding(-16) // ...but remove it to not affect layout.
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.accessibilityLabel(Localizations.close)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Button {
|
|
||||||
actionTapped()
|
|
||||||
} label: {
|
|
||||||
Text(buttonText)
|
|
||||||
}
|
|
||||||
.buttonStyle(.primary())
|
|
||||||
|
|
||||||
if let secondaryButtonText, let secondaryActionTapped {
|
|
||||||
Button {
|
|
||||||
secondaryActionTapped()
|
|
||||||
} label: {
|
|
||||||
Text(secondaryButtonText)
|
|
||||||
}
|
|
||||||
.buttonStyle(.bitwardenBorderless)
|
|
||||||
.padding(.bottom, -8) // Remove extra padding below the borderless button.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.background {
|
|
||||||
Asset.Colors.backgroundPrimary.swiftUIColor
|
|
||||||
.clipShape(.rect(cornerRadius: 16))
|
|
||||||
.shadow(color: .black.opacity(0.45), radius: 2, x: 0, y: 1)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Previews
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
struct ItemListCardView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ScrollView {
|
|
||||||
VStack {
|
|
||||||
ItemListCardView(
|
|
||||||
bodyText: Localizations
|
|
||||||
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
|
|
||||||
buttonText: Localizations.takeMeToTheAppSettings,
|
|
||||||
leftImage: {
|
|
||||||
Image(decorative: SharedAsset.Icons.arrowSync24)
|
|
||||||
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
},
|
|
||||||
titleText: Localizations.syncWithTheBitwardenApp,
|
|
||||||
actionTapped: {},
|
|
||||||
closeTapped: {},
|
|
||||||
)
|
|
||||||
|
|
||||||
ItemListCardView(
|
|
||||||
bodyText: Localizations
|
|
||||||
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
|
|
||||||
buttonText: Localizations.takeMeToTheAppSettings,
|
|
||||||
leftImage: {
|
|
||||||
Image(decorative: SharedAsset.Icons.arrowSync24)
|
|
||||||
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
},
|
|
||||||
secondaryButtonText: Localizations.learnMore,
|
|
||||||
titleText: Localizations.syncWithTheBitwardenApp,
|
|
||||||
actionTapped: {},
|
|
||||||
closeTapped: {},
|
|
||||||
secondaryActionTapped: {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@ -43,7 +43,7 @@ class ItemListViewTests: BitwardenTestCase {
|
|||||||
|
|
||||||
/// Test the close taps trigger the associated effect.
|
/// Test the close taps trigger the associated effect.
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_itemListCardView_close_download() throws {
|
func test_actionCard_close_download() async throws {
|
||||||
let state = ItemListState(
|
let state = ItemListState(
|
||||||
itemListCardState: .passwordManagerDownload,
|
itemListCardState: .passwordManagerDownload,
|
||||||
loadingState: .data([ItemListSection.fixture()]),
|
loadingState: .data([ItemListSection.fixture()]),
|
||||||
@ -54,16 +54,15 @@ class ItemListViewTests: BitwardenTestCase {
|
|||||||
timeProvider: timeProvider,
|
timeProvider: timeProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap()
|
let actionCard = try subject.inspect().find(actionCard: Localizations.downloadTheBitwardenApp)
|
||||||
|
try await actionCard.find(asyncButton: Localizations.close).tap()
|
||||||
waitFor(!processor.effects.isEmpty)
|
|
||||||
|
|
||||||
XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerDownload))
|
XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerDownload))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test the close taps trigger the associated effect.
|
/// Test the close taps trigger the associated effect.
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_itemListCardView_close_sync() throws {
|
func test_actionCard_close_sync() async throws {
|
||||||
let state = ItemListState(
|
let state = ItemListState(
|
||||||
itemListCardState: .passwordManagerSync,
|
itemListCardState: .passwordManagerSync,
|
||||||
loadingState: .data([]),
|
loadingState: .data([]),
|
||||||
@ -74,9 +73,8 @@ class ItemListViewTests: BitwardenTestCase {
|
|||||||
timeProvider: timeProvider,
|
timeProvider: timeProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
try subject.inspect().find(buttonWithAccessibilityLabel: Localizations.close).tap()
|
let actionCard = try subject.inspect().find(actionCard: Localizations.syncWithTheBitwardenApp)
|
||||||
|
try await actionCard.find(asyncButton: Localizations.close).tap()
|
||||||
waitFor(!processor.effects.isEmpty)
|
|
||||||
|
|
||||||
XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerSync))
|
XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerSync))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import SwiftUI
|
|||||||
// MARK: - SearchableItemListView
|
// MARK: - SearchableItemListView
|
||||||
|
|
||||||
/// A view that displays the items in a single vault group.
|
/// A view that displays the items in a single vault group.
|
||||||
private struct SearchableItemListView: View { // swiftlint:disable:this type_body_length
|
private struct SearchableItemListView: View {
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// A flag indicating if the search bar is focused.
|
/// A flag indicating if the search bar is focused.
|
||||||
@ -131,52 +131,42 @@ private struct SearchableItemListView: View { // swiftlint:disable:this type_bod
|
|||||||
|
|
||||||
/// The Password Manager download card definition.
|
/// The Password Manager download card definition.
|
||||||
private var itemListCardPasswordManagerInstall: some View {
|
private var itemListCardPasswordManagerInstall: some View {
|
||||||
ItemListCardView(
|
ActionCard(
|
||||||
bodyText: Localizations.storeAllOfYourLoginsAndSyncVerificationCodesDirectlyWithTheAuthenticatorApp,
|
title: Localizations.downloadTheBitwardenApp,
|
||||||
buttonText: Localizations.downloadTheBitwardenApp,
|
message: Localizations.storeAllOfYourLoginsAndSyncVerificationCodesDirectlyWithTheAuthenticatorApp,
|
||||||
leftImage: {
|
actionButtonState: ActionCard.ButtonState(title: Localizations.downloadTheBitwardenApp) {
|
||||||
Image(decorative: SharedAsset.Icons.shield24)
|
|
||||||
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
},
|
|
||||||
titleText: Localizations.downloadTheBitwardenApp,
|
|
||||||
actionTapped: {
|
|
||||||
openURL(ExternalLinksConstants.passwordManagerLink)
|
openURL(ExternalLinksConstants.passwordManagerLink)
|
||||||
},
|
},
|
||||||
closeTapped: {
|
dismissButtonState: ActionCard.ButtonState(title: Localizations.close) {
|
||||||
Task {
|
await store.perform(.closeCard(.passwordManagerDownload))
|
||||||
await store.perform(.closeCard(.passwordManagerDownload))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
) {
|
||||||
|
Image(decorative: SharedAsset.Icons.shield24)
|
||||||
|
.foregroundColor(SharedAsset.Colors.iconSecondary.swiftUIColor)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Password Manager sync card definition.
|
/// The Password Manager sync card definition.
|
||||||
private var itemListCardSync: some View {
|
private var itemListCardSync: some View {
|
||||||
ItemListCardView(
|
ActionCard(
|
||||||
bodyText: Localizations
|
title: Localizations.syncWithTheBitwardenApp,
|
||||||
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
|
message: Localizations.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
|
||||||
buttonText: Localizations.takeMeToTheAppSettings,
|
actionButtonState: ActionCard.ButtonState(title: Localizations.takeMeToTheAppSettings) {
|
||||||
leftImage: {
|
|
||||||
Image(decorative: SharedAsset.Icons.arrowSync24)
|
|
||||||
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
},
|
|
||||||
secondaryButtonText: Localizations.learnMore,
|
|
||||||
titleText: Localizations.syncWithTheBitwardenApp,
|
|
||||||
actionTapped: {
|
|
||||||
openURL(ExternalLinksConstants.passwordManagerSettings)
|
openURL(ExternalLinksConstants.passwordManagerSettings)
|
||||||
},
|
},
|
||||||
closeTapped: {
|
dismissButtonState: ActionCard.ButtonState(title: Localizations.close) {
|
||||||
Task {
|
await store.perform(.closeCard(.passwordManagerSync))
|
||||||
await store.perform(.closeCard(.passwordManagerSync))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
secondaryActionTapped: {
|
secondaryButtonState: ActionCard.ButtonState(title: Localizations.learnMore) {
|
||||||
openURL(ExternalLinksConstants.totpSyncHelp)
|
openURL(ExternalLinksConstants.totpSyncHelp)
|
||||||
},
|
},
|
||||||
)
|
) {
|
||||||
|
Image(decorative: SharedAsset.Icons.arrowSync24)
|
||||||
|
.foregroundColor(SharedAsset.Colors.iconSecondary.swiftUIColor)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -722,6 +712,36 @@ struct ItemListView_Previews: PreviewProvider { // swiftlint:disable:this type_b
|
|||||||
timeProvider: PreviewTimeProvider(),
|
timeProvider: PreviewTimeProvider(),
|
||||||
)
|
)
|
||||||
}.previewDisplayName("SharedItems")
|
}.previewDisplayName("SharedItems")
|
||||||
|
|
||||||
|
NavigationView {
|
||||||
|
ItemListView(
|
||||||
|
store: Store(
|
||||||
|
processor: StateProcessor(
|
||||||
|
state: ItemListState(
|
||||||
|
itemListCardState: .passwordManagerDownload,
|
||||||
|
loadingState: .data([]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timeProvider: PreviewTimeProvider(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Password Manager Download Card")
|
||||||
|
|
||||||
|
NavigationView {
|
||||||
|
ItemListView(
|
||||||
|
store: Store(
|
||||||
|
processor: StateProcessor(
|
||||||
|
state: ItemListState(
|
||||||
|
itemListCardState: .passwordManagerSync,
|
||||||
|
loadingState: .data([]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timeProvider: PreviewTimeProvider(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Password Manager Sync Card")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 167 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 584 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
@ -18,12 +18,22 @@ public struct BitwardenBorderlessButtonStyle: ButtonStyle {
|
|||||||
: SharedAsset.Colors.buttonOutlinedDisabledForeground.swiftUIColor
|
: SharedAsset.Colors.buttonOutlinedDisabledForeground.swiftUIColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If this button should fill to take up as much width as possible.
|
||||||
|
var shouldFillWidth = false
|
||||||
|
|
||||||
|
/// The size of the button.
|
||||||
|
var size: ButtonStyleSize?
|
||||||
|
|
||||||
// MARK: ButtonStyle
|
// MARK: ButtonStyle
|
||||||
|
|
||||||
public func makeBody(configuration: Configuration) -> some View {
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
configuration.label
|
configuration.label
|
||||||
.foregroundStyle(foregroundColor)
|
.foregroundStyle(foregroundColor)
|
||||||
.styleGuide(.subheadlineSemibold)
|
.styleGuide(size?.fontStyle ?? .subheadlineSemibold)
|
||||||
|
.padding(.vertical, size?.verticalPadding ?? 0)
|
||||||
|
.padding(.horizontal, size?.horizontalPadding ?? 0)
|
||||||
|
.frame(maxWidth: shouldFillWidth ? .infinity : nil, minHeight: size?.minimumHeight ?? nil)
|
||||||
|
.contentShape(Capsule())
|
||||||
.opacity(configuration.isPressed ? 0.5 : 1)
|
.opacity(configuration.isPressed ? 0.5 : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,15 +43,31 @@ public struct BitwardenBorderlessButtonStyle: ButtonStyle {
|
|||||||
public extension ButtonStyle where Self == BitwardenBorderlessButtonStyle {
|
public extension ButtonStyle where Self == BitwardenBorderlessButtonStyle {
|
||||||
/// The style for a borderless button in this application.
|
/// The style for a borderless button in this application.
|
||||||
///
|
///
|
||||||
|
/// This style does not add any padding to the button. Padding should be applied by the caller.
|
||||||
|
///
|
||||||
static var bitwardenBorderless: BitwardenBorderlessButtonStyle {
|
static var bitwardenBorderless: BitwardenBorderlessButtonStyle {
|
||||||
BitwardenBorderlessButtonStyle()
|
BitwardenBorderlessButtonStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The style for a borderless button in this application with padding and font size based on
|
||||||
|
/// the button size.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - shouldFillWidth: A flag indicating if this button should fill all available space.
|
||||||
|
/// - size: The size of the button, which determines the padding and font size applied.
|
||||||
|
///
|
||||||
|
static func bitwardenBorderless(
|
||||||
|
shouldFillWidth: Bool = true,
|
||||||
|
size: ButtonStyleSize,
|
||||||
|
) -> BitwardenBorderlessButtonStyle {
|
||||||
|
BitwardenBorderlessButtonStyle(shouldFillWidth: shouldFillWidth, size: size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Previews
|
// MARK: Previews
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
#Preview() {
|
#Preview("States") {
|
||||||
VStack {
|
VStack {
|
||||||
Button("Bitwarden") {}
|
Button("Bitwarden") {}
|
||||||
|
|
||||||
@ -49,6 +75,20 @@ public extension ButtonStyle where Self == BitwardenBorderlessButtonStyle {
|
|||||||
.disabled(true)
|
.disabled(true)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bitwardenBorderless)
|
.buttonStyle(.bitwardenBorderless)
|
||||||
.padding(.vertical, 14)
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Sizes") {
|
||||||
|
VStack {
|
||||||
|
Button("Small") {}
|
||||||
|
.buttonStyle(.bitwardenBorderless(size: .small))
|
||||||
|
|
||||||
|
Button("Medium") {}
|
||||||
|
.buttonStyle(.bitwardenBorderless(size: .medium))
|
||||||
|
|
||||||
|
Button("Large") {}
|
||||||
|
.buttonStyle(.bitwardenBorderless(size: .large))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
// swiftlint:disable:this file_name
|
// swiftlint:disable:this file_name
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import ViewInspectorTestHelpers
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
final class ActionCardTests: BitwardenTestCase {
|
final class ActionCardTests: BitwardenTestCase {
|
||||||
// MARK: Tests
|
// MARK: Tests
|
||||||
@ -38,4 +39,22 @@ final class ActionCardTests: BitwardenTestCase {
|
|||||||
|
|
||||||
XCTAssertTrue(dismissButtonTapped)
|
XCTAssertTrue(dismissButtonTapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tapping the secondary button should call the secondary button state's action closure.
|
||||||
|
@MainActor
|
||||||
|
func test_secondaryButton_tap() async throws {
|
||||||
|
var secondaryButtonTapped = false
|
||||||
|
let subject = ActionCard(
|
||||||
|
title: "Title",
|
||||||
|
message: "Message",
|
||||||
|
secondaryButtonState: ActionCard.ButtonState(title: "Secondary") {
|
||||||
|
secondaryButtonTapped = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
let button = try subject.inspect().find(asyncButton: "Secondary")
|
||||||
|
try await button.tap()
|
||||||
|
|
||||||
|
XCTAssertTrue(secondaryButtonTapped)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import BitwardenKit
|
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -6,12 +5,12 @@ import SwiftUI
|
|||||||
|
|
||||||
/// A view that displays a card representing an action that the user needs to take.
|
/// A view that displays a card representing an action that the user needs to take.
|
||||||
///
|
///
|
||||||
struct ActionCard<LeadingContent: View>: View {
|
public struct ActionCard<LeadingContent: View>: View {
|
||||||
// MARK: Types
|
// MARK: Types
|
||||||
|
|
||||||
/// A data model containing the properties for a button within an action card.
|
/// A data model containing the properties for a button within an action card.
|
||||||
///
|
///
|
||||||
struct ButtonState {
|
public struct ButtonState {
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// An action to perform when the button is tapped.
|
/// An action to perform when the button is tapped.
|
||||||
@ -28,7 +27,7 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
/// - title: The title of the button.
|
/// - title: The title of the button.
|
||||||
/// - action: An action to perform when the button is tapped.
|
/// - action: An action to perform when the button is tapped.
|
||||||
///
|
///
|
||||||
init(title: String, action: @escaping () async -> Void) {
|
public init(title: String, action: @escaping () async -> Void) {
|
||||||
self.action = action
|
self.action = action
|
||||||
self.title = title
|
self.title = title
|
||||||
}
|
}
|
||||||
@ -48,12 +47,15 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
/// The message to display in the card, below the title.
|
/// The message to display in the card, below the title.
|
||||||
let message: String?
|
let message: String?
|
||||||
|
|
||||||
|
/// State that describes the secondary button.
|
||||||
|
let secondaryButtonState: ButtonState?
|
||||||
|
|
||||||
/// The title of the card.
|
/// The title of the card.
|
||||||
let title: String
|
let title: String
|
||||||
|
|
||||||
// MARK: View
|
// MARK: View
|
||||||
|
|
||||||
var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
if let leadingContent {
|
if let leadingContent {
|
||||||
@ -82,9 +84,18 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let actionButtonState {
|
if actionButtonState != nil || secondaryButtonState != nil {
|
||||||
AsyncButton(actionButtonState.title, action: actionButtonState.action)
|
VStack(spacing: 4) {
|
||||||
.buttonStyle(.primary(size: .medium))
|
if let actionButtonState {
|
||||||
|
AsyncButton(actionButtonState.title, action: actionButtonState.action)
|
||||||
|
.buttonStyle(.primary(size: .medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let secondaryButtonState {
|
||||||
|
AsyncButton(secondaryButtonState.title, action: secondaryButtonState.action)
|
||||||
|
.buttonStyle(.bitwardenBorderless(size: .medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(SharedAsset.Colors.textPrimary.swiftUIColor)
|
.foregroundStyle(SharedAsset.Colors.textPrimary.swiftUIColor)
|
||||||
@ -108,19 +119,22 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
/// - message: The message to display in the card.
|
/// - message: The message to display in the card.
|
||||||
/// - actionButtonState: State that describes the action button.
|
/// - actionButtonState: State that describes the action button.
|
||||||
/// - dismissButtonState: State that describes the dismiss button.
|
/// - dismissButtonState: State that describes the dismiss button.
|
||||||
|
/// - secondaryButtonState: State that describes the secondary button.
|
||||||
/// - leadingContent: Content that is displayed at the leading edge of the title and message.
|
/// - leadingContent: Content that is displayed at the leading edge of the title and message.
|
||||||
///
|
///
|
||||||
init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
message: String? = nil,
|
message: String? = nil,
|
||||||
actionButtonState: ButtonState? = nil,
|
actionButtonState: ButtonState? = nil,
|
||||||
dismissButtonState: ButtonState? = nil,
|
dismissButtonState: ButtonState? = nil,
|
||||||
|
secondaryButtonState: ButtonState? = nil,
|
||||||
@ViewBuilder leadingContent: () -> LeadingContent,
|
@ViewBuilder leadingContent: () -> LeadingContent,
|
||||||
) {
|
) {
|
||||||
self.actionButtonState = actionButtonState
|
self.actionButtonState = actionButtonState
|
||||||
self.dismissButtonState = dismissButtonState
|
self.dismissButtonState = dismissButtonState
|
||||||
self.leadingContent = leadingContent()
|
self.leadingContent = leadingContent()
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.secondaryButtonState = secondaryButtonState
|
||||||
self.title = title
|
self.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,17 +145,20 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
/// - message: The message to display in the card.
|
/// - message: The message to display in the card.
|
||||||
/// - actionButtonState: State that describes the action button.
|
/// - actionButtonState: State that describes the action button.
|
||||||
/// - dismissButtonState: State that describes the dismiss button.
|
/// - dismissButtonState: State that describes the dismiss button.
|
||||||
|
/// - secondaryButtonState: State that describes the secondary button.
|
||||||
///
|
///
|
||||||
init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
message: String? = nil,
|
message: String? = nil,
|
||||||
actionButtonState: ButtonState? = nil,
|
actionButtonState: ButtonState? = nil,
|
||||||
dismissButtonState: ButtonState? = nil,
|
dismissButtonState: ButtonState? = nil,
|
||||||
|
secondaryButtonState: ButtonState? = nil,
|
||||||
) where LeadingContent == EmptyView {
|
) where LeadingContent == EmptyView {
|
||||||
self.actionButtonState = actionButtonState
|
self.actionButtonState = actionButtonState
|
||||||
self.dismissButtonState = dismissButtonState
|
self.dismissButtonState = dismissButtonState
|
||||||
leadingContent = nil
|
leadingContent = nil
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.secondaryButtonState = secondaryButtonState
|
||||||
self.title = title
|
self.title = title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,6 +180,14 @@ struct ActionCard<LeadingContent: View>: View {
|
|||||||
dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {},
|
dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ActionCard(
|
||||||
|
title: "Title",
|
||||||
|
message: "Message",
|
||||||
|
actionButtonState: ActionCard.ButtonState(title: "Tap me!") {},
|
||||||
|
dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {},
|
||||||
|
secondaryButtonState: ActionCard.ButtonState(title: "Secondary button") {},
|
||||||
|
)
|
||||||
|
|
||||||
ActionCard(
|
ActionCard(
|
||||||
title: "Title",
|
title: "Title",
|
||||||
message: "Message",
|
message: "Message",
|
||||||
@ -12,7 +12,7 @@ public struct ActionCardType: BaseViewType {
|
|||||||
public static var typePrefix: String = "ActionCard"
|
public static var typePrefix: String = "ActionCard"
|
||||||
|
|
||||||
public static var namespacedPrefixes: [String] = [
|
public static var namespacedPrefixes: [String] = [
|
||||||
"BitwardenShared.ActionCard",
|
"BitwardenKit.ActionCard",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user