Skip to content

[PM-17659] [RC] Add learn more links for TOTP code syncing #1601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ enum ExternalLinksConstants {

/// A markdown link to Bitwarden's terms of service.
static let termsOfService = URL(string: "https://bitwarden.com/terms/")!

/// A link to the Bitwarden Help Center page for syncing TOTP codes between PM and Authenticator.
static let totpSyncHelp = URL(string: "https://bitwarden.com/help/totp-sync/")!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import SwiftUI

// MARK: - BitwardenBorderlessButtonStyle

/// The style for a borderless button in this application.
///
struct BitwardenBorderlessButtonStyle: ButtonStyle {
// MARK: Properties

/// A value indicating whether the button is currently enabled or disabled.
@Environment(\.isEnabled) var isEnabled: Bool

// MARK: ButtonStyle

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(Asset.Colors.primaryBitwarden.swiftUIColor)
.padding(.vertical, 14)
.padding(.horizontal, 20)
.opacity(configuration.isPressed ? 0.5 : 1)
}
}

// MARK: ButtonStyle

extension ButtonStyle where Self == BitwardenBorderlessButtonStyle {
/// The style for a borderless button in this application.
///
static var bitwardenBorderless: BitwardenBorderlessButtonStyle {
BitwardenBorderlessButtonStyle()
}
}

// MARK: Previews

#if DEBUG
#Preview() {
VStack {
Button("Bitwarden") {}

Button("Bitwarden") {}
.disabled(true)
}
.buttonStyle(.bitwardenBorderless)
.padding(.vertical, 14)
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct PrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(foregroundColor)
.multilineTextAlignment(.center)
.padding(.vertical, 14)
.padding(.horizontal, 20)
.frame(maxWidth: shouldFillWidth ? .infinity : nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@
"NeedHelpVisitOurHelpCenterForGuidance" = "Need help? Visit our Help Center for guidance.";
"UnexpectedDataFormat" = "Unexpected Data Format";
"TheDataFormatProvidedDoesntMatchWhatsExpected" = "The data format provided doesnโ€™t match whatโ€™s expected. Please check your file and ensure the correct data types are used.";
"ThisFeatureIsNotYetAvailableForSelfHostedUsers" = "This feature is not yet available for self-hosted users. [Learn more](%1$@)";
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,7 @@ struct SettingsView: View {
}

if store.state.shouldShowSyncButton {
externalLinkRow(
Localizations.syncWithBitwardenApp,
action: .syncWithBitwardenAppTapped,
hasDivider: store.state.shouldShowDefaultSaveOption
)
syncWithPasswordManagerRow(hasDivider: store.state.shouldShowDefaultSaveOption)
defaultSaveOption
}
}
Expand Down Expand Up @@ -250,6 +246,44 @@ struct SettingsView: View {
.imageStyle(.rowIcon)
}
}

/// The settings row for syncing with the Password Manager app.
private func syncWithPasswordManagerRow(hasDivider: Bool) -> some View {
Button {
store.send(.syncWithBitwardenAppTapped)
} label: {
VStack(spacing: 0) {
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 0) {
Text(Localizations.syncWithBitwardenApp)
.styleGuide(.body)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)

Text(LocalizedStringKey(
Localizations.thisFeatureIsNotYetAvailableForSelfHostedUsers(
ExternalLinksConstants.totpSyncHelp
)
))
.styleGuide(.subheadline)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.tint(Asset.Colors.primaryBitwarden.swiftUIColor)
}
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)

Asset.Images.externalLink2.swiftUIImage
.imageStyle(.rowIcon)
}
.padding(16)

if hasDivider {
Divider()
.padding(.leading, 16)
}
}
}
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
}
}

// MARK: - Previews
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
/// 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

Expand All @@ -27,43 +30,58 @@
/// 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 {
HStack(alignment: .top, spacing: 16) {
leftImage
.padding(.leading, 16)
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)
}

VStack(alignment: .leading, spacing: 0) {
Group {
Text(titleText)
.styleGuide(.headline)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
Button {
closeTapped()
} label: {
Image(decorative: Asset.Images.cancel)
.padding(16) // Add padding to increase tappable area...
}
.padding(-16) // ...but remove it to not affect layout.
.buttonStyle(PlainButtonStyle())
.accessibilityLabel(Localizations.close)
}

Text(bodyText)
.styleGuide(.subheadline)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
VStack(spacing: 0) {
Button {
actionTapped()
} label: {
Text(buttonText)
}
.buttonStyle(.primary())

if let secondaryButtonText, let secondaryActionTapped {
Button {
actionTapped()
secondaryActionTapped()

Check warning on line 75 in AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView.swift

View check run for this annotation

Codecov / codecov/patch

AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListCardView.swift#L75

Added line #L75 was not covered by tests
} label: {
Text(buttonText)
.foregroundColor(Asset.Colors.primaryBitwarden.swiftUIColor)
.styleGuide(.subheadline, weight: .semibold)
Text(secondaryButtonText)
}
.padding(.top, 8)
.buttonStyle(.bitwardenBorderless)
.padding(.bottom, -8) // Remove extra padding below the borderless button.
}
.frame(maxWidth: .infinity, alignment: .leading)
}

Button {
closeTapped()
} label: {
Image(decorative: Asset.Images.cancel)
.padding(.trailing, 16)
}
.buttonStyle(PlainButtonStyle())
.accessibilityLabel(Localizations.close)
}
.padding(.vertical, 16)
.padding(16)
.background {
Asset.Colors.backgroundPrimary.swiftUIColor
.clipShape(.rect(cornerRadius: 16))
Expand All @@ -78,20 +96,40 @@
#if DEBUG
struct ItemListCardView_Previews: PreviewProvider {
static var previews: some View {
ItemListCardView(
bodyText: Localizations
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
buttonText: Localizations.takeMeToTheAppSettings,
leftImage: {
Image(decorative: Asset.Images.syncArrow)
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
.frame(width: 24, height: 24)
},
titleText: Localizations.syncWithTheBitwardenApp,
actionTapped: {},
closeTapped: {}
)
.padding(16)
ScrollView {
VStack {
ItemListCardView(
bodyText: Localizations
.allowAuthenticatorAppSyncingInSettingsToViewAllYourVerificationCodesHere,
buttonText: Localizations.takeMeToTheAppSettings,
leftImage: {
Image(decorative: Asset.Images.syncArrow)
.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: Asset.Images.syncArrow)
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
.frame(width: 24, height: 24)
},
secondaryButtonText: Localizations.learnMore,
titleText: Localizations.syncWithTheBitwardenApp,
actionTapped: {},
closeTapped: {},
secondaryActionTapped: {}
)
}
.padding(16)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ItemListCardViewTests: BitwardenTestCase {
as: [
"\(name)-portrait": .defaultPortrait,
"\(name)-portraitDark": .defaultPortraitDark,
"\(name)-portraitAX5": .defaultPortraitAX5,
"\(name)-portraitAX5": .tallPortraitAX5(heightMultiple: 3),
]
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// MARK: - SearchableItemListView

/// A view that displays the items in a single vault group.
private struct SearchableItemListView: View {
private struct SearchableItemListView: View { // swiftlint:disable:this type_body_length
// MARK: Properties

/// A flag indicating if the search bar is focused.
Expand Down Expand Up @@ -162,6 +162,7 @@
.foregroundColor(Asset.Colors.primaryBitwardenLight.swiftUIColor)
.frame(width: 24, height: 24)
},
secondaryButtonText: Localizations.learnMore,
titleText: Localizations.syncWithTheBitwardenApp,
actionTapped: {
openURL(ExternalLinksConstants.passwordManagerSettings)
Expand All @@ -170,6 +171,9 @@
Task {
await store.perform(.closeCard(.passwordManagerSync))
}
},
secondaryActionTapped: {
openURL(ExternalLinksConstants.totpSyncHelp)

Check warning on line 176 in AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift

View check run for this annotation

Codecov / codecov/patch

AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListView.swift#L176

Added line #L176 was not covered by tests
}
)
.padding(.top, 16)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.