diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 8fccc88d53..57de95a451 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -6626,6 +6626,54 @@ } } }, + "pm.add.card" : { + "comment" : "Add New Credit Card button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Credit Card" + } + } + } + }, + "pm.add.identity" : { + "comment" : "Add New Identity button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Identity" + } + } + } + }, + "pm.add.login" : { + "comment" : "Add New Login button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Password" + } + } + } + }, + "pm.add.new" : { + "comment" : "Add New item button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add New" + } + } + } + }, "pm.added" : { "comment" : "Label for login added data", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift index 23de1bdfa9..39f7872e27 100644 --- a/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift +++ b/DuckDuckGo/SecureVault/Extensions/UserText+PasswordManager.swift @@ -33,6 +33,10 @@ extension UserText { static let pmEmptyStateCardsTitle = NSLocalizedString("pm.empty.cards.title", value: "No Cards", comment: "Label for cards empty state title") static let pmEmptyStateNotesTitle = NSLocalizedString("pm.empty.notes.title", value: "No Notes", comment: "Label for notes empty state title") + static let pmAddItem = NSLocalizedString("pm.add.new", value: "Add New", comment: "Add New item button") + static let pmAddCard = NSLocalizedString("pm.add.card", value: "Add Credit Card", comment: "Add New Credit Card button") + static let pmAddLogin = NSLocalizedString("pm.add.login", value: "Add Password", comment: "Add New Login button") + static let pmAddIdentity = NSLocalizedString("pm.add.identity", value: "Add Identity", comment: "Add New Identity button") static let pmNewCard = NSLocalizedString("pm.new.card", value: "Credit Card", comment: "Label for new card title") static let pmNewLogin = NSLocalizedString("pm.new.login", value: "Password", comment: "Label for new login title") static let pmNewIdentity = NSLocalizedString("pm.new.identity", value: "Identity", comment: "Label for new identity title") diff --git a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift index 0c401b67ea..4f0b2573f2 100644 --- a/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/SecureVault/Model/PasswordManagementItemListModel.swift @@ -275,15 +275,18 @@ final class PasswordManagementItemListModel: ObservableObject { @Published var canChangeCategory: Bool = true private var onItemSelected: (_ old: SecureVaultItem?, _ new: SecureVaultItem?) -> Void + private var onAddItemSelected: (_ category: SecureVaultSorting.Category) -> Void private let tld: TLD private let urlMatcher: AutofillDomainNameUrlMatcher private static let randomColorsCount = 15 init(passwordManagerCoordinator: PasswordManagerCoordinating, - onItemSelected: @escaping (_ old: SecureVaultItem?, _ new: SecureVaultItem?) -> Void, urlMatcher: AutofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher(), - tld: TLD = ContentBlocking.shared.tld) { + tld: TLD = ContentBlocking.shared.tld, + onItemSelected: @escaping (_ old: SecureVaultItem?, _ new: SecureVaultItem?) -> Void, + onAddItemSelected: @escaping (_ category: SecureVaultSorting.Category) -> Void) { self.onItemSelected = onItemSelected + self.onAddItemSelected = onAddItemSelected self.passwordManagerCoordinator = passwordManagerCoordinator self.urlMatcher = urlMatcher self.tld = tld @@ -479,4 +482,8 @@ final class PasswordManagementItemListModel: ObservableObject { return tld.eTLDplus1(name) ?? title } + func onAddItemClickedFor(_ category: SecureVaultSorting.Category) { + onAddItemSelected(category) + } + } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift index 6c74891652..85454df87b 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementCreditCardItemView.swift @@ -47,6 +47,7 @@ struct PasswordManagementCreditCardItemView: View { VStack(alignment: .leading, spacing: 0) { HeaderView() + .padding(.top, 16) .padding(.bottom, model.isInEditMode ? 20 : 30) EditableCreditCardField(textFieldValue: $model.cardNumber, title: UserText.pmCardNumber) @@ -94,10 +95,12 @@ struct PasswordManagementCreditCardItemView: View { Spacer(minLength: 0) Buttons() + .padding(.top, model.isInEditMode ? 12 : 10) + .padding(.bottom, model.isInEditMode ? 12 : 3) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() + .padding(.horizontal) } .padding(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 10)) diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift index cbe39fcefa..c82e1b5126 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementIdentityItemView.swift @@ -72,7 +72,9 @@ struct PasswordManagementIdentityItemView: View { } Buttons() - .padding() + .padding(.top, editMode ? 4 : 10) + .padding(.bottom, editMode ? 12 : 3) + .padding(.horizontal) } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift b/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift index b4acbbea15..c717288450 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementItemList.swift @@ -72,6 +72,14 @@ struct PasswordManagementItemListView: View { } } } + + Spacer(minLength: 0) + + Divider() + + PasswordManagementAddButton() + .padding() + } } @@ -131,6 +139,7 @@ struct PasswordManagementItemListCategoryView: View { PasswordManagementSortButton(imageName: "SortDescending") } } + .padding(.vertical, -4) } } @@ -392,3 +401,75 @@ struct PasswordManagementSortButton: View { } } + +private struct PasswordManagementAddButton: View { + + @EnvironmentObject var model: PasswordManagementItemListModel + + var body: some View { + + switch model.sortDescriptor.category { + case .allItems: + ZStack { + // Setting Menu label to empty string and overlaying with this as Menu will not allow the image + text to be centered + Text(UserText.pmAddItem) + + Menu { + createMenuItem(imageName: "LoginGlyph", text: UserText.pmNewLogin, category: .logins) + createMenuItem(imageName: "IdentityGlyph", text: UserText.pmNewIdentity, category: .identities) + createMenuItem(imageName: "CreditCardGlyph", text: UserText.pmNewCard, category: .cards) + } label: { + Text("") + } + .modifier(HideMenuIndicatorModifier()) + } + .padding(.vertical, -4) + case .logins: + createButton(text: UserText.pmAddLogin, category: model.sortDescriptor.category) + case .identities: + createButton(text: UserText.pmAddIdentity, category: model.sortDescriptor.category) + case .cards: + createButton(text: UserText.pmAddCard, category: model.sortDescriptor.category) + } + + } + + private func createMenuItem(imageName: String, text: String, category: SecureVaultSorting.Category) -> some View { + + Button { + model.onAddItemClickedFor(category) + } label: { + HStack { + Image(imageName) + Text(text) + } + } + + } + + private func createButton(text: String, category: SecureVaultSorting.Category) -> some View { + + Button { + model.onAddItemClickedFor(category) + } label: { + Text(text) + .frame(maxWidth: .infinity) + .offset(y: 1) + } + .padding(.vertical, -4) + + } +} + +private struct HideMenuIndicatorModifier: ViewModifier { + + func body(content: Content) -> some View { + if #available(macOS 12, *) { + content + .menuIndicator(.hidden) + } else { + content + } + } + +} diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift b/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift index 1352b5f510..11e130f21c 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementLoginItemView.swift @@ -83,7 +83,9 @@ struct PasswordManagementLoginItemView: View { } Buttons() - .padding() + .padding(.top, editMode ? 12 : 10) + .padding(.bottom, editMode ? 12 : 3) + .padding(.horizontal) } } diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index a2411dab35..7b07c3c7f8 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -689,8 +689,9 @@ final class PasswordManagementViewController: NSViewController { var passwordManagerSelectionCancellable: AnyCancellable? // swiftlint:disable function_body_length + // swiftlint:disable:next cyclomatic_complexity private func createListView() { - let listModel = PasswordManagementItemListModel(passwordManagerCoordinator: self.passwordManagerCoordinator) { [weak self] previousValue, newValue in + let listModel = PasswordManagementItemListModel(passwordManagerCoordinator: self.passwordManagerCoordinator, onItemSelected: { [weak self] previousValue, newValue in guard let newValue = newValue, let id = newValue.secureVaultID, let window = self?.view.window else { @@ -748,7 +749,18 @@ final class PasswordManagementViewController: NSViewController { } else { loadNewItemWithID() } - } + }, onAddItemSelected: { [weak self] category in + switch category { + case .logins: + self?.createNewLogin() + case .identities: + self?.createNewIdentity() + case .cards: + self?.createNewCreditCard() + default: + break + } + }) self.listModel = listModel self.listView = NSHostingView(rootView: PasswordManagementItemListView().environmentObject(listModel)) diff --git a/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift b/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift index 4c049e48e1..553b1ad84d 100644 --- a/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift +++ b/UnitTests/SecureVault/PasswordManagementItemListModelTests.swift @@ -27,7 +27,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { func testWhenAccountIsSelectedThenModelReflectsThat() { let accounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) model.select(item: accounts[0]) @@ -47,7 +47,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { func testWhenFilterAppliedThenDisplayedAccountsOnlyContainFilteredMatches() { let createdAccounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: createdAccounts) model.filter = "domain5" @@ -66,7 +66,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { } func testWhenAccountIsSelectedThenCallbackReceivesOldAndNewVersion() { - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) let account = makeAccount(id: 1) model.update(items: [account]) @@ -77,12 +77,12 @@ final class PasswordManagementItemListModelTests: XCTestCase { } func testWhenGettingEmptyState_AndViewModelIsNewlyCreated_ThenEmptyStateIsNone() { - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) XCTAssertEqual(model.emptyState, .none) } func testWhenGettingEmptyState_AndViewModelGetsGivenEmptyDataSet_ThenEmptyStateIsNoData() { - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: []) XCTAssertEqual(model.emptyState, .noData) @@ -90,7 +90,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { func testWhenGettingEmptyState_AndViewModelHasData_AndCategoryIsAllItems_AndViewModelIsFiltered_ThenEmptyStateIsNone() { let accounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) XCTAssertEqual(model.emptyState, .none) @@ -104,7 +104,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { func testWhenGettingEmptyState_AndViewModelHasData_AndCategoryIsLogins_AndViewModelIsFiltered_ThenEmptyStateIsLogins() { let accounts = (0 ..< 10).map { makeAccount(id: $0, domain: "domain\($0)") } - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) model.sortDescriptor.category = .logins @@ -123,7 +123,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { let account3 = makeAccount(id: 3, domain: "otherdomain.com") let accounts = [account1, account2, account3] - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) model.selectLoginWithDomainOrFirst(domain: "dummy.com") @@ -138,7 +138,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { let account3 = makeAccount(id: 3, domain: "otherdomain.com") let accounts = [account1, account2, account3] - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) model.selectLoginWithDomainOrFirst(domain: "example.com") @@ -153,7 +153,7 @@ final class PasswordManagementItemListModelTests: XCTestCase { let account3 = makeAccount(id: 3, domain: "www.example.com") let accounts = [account1, account2, account3] - let model = PasswordManagementItemListModel(onItemSelected: onItemSelected) + let model = PasswordManagementItemListModel(onItemSelected: onItemSelected, onAddItemSelected: onAddItemSelected) model.update(items: accounts) model.selectLoginWithDomainOrFirst(domain: "sub.example.com") @@ -180,6 +180,8 @@ final class PasswordManagementItemListModelTests: XCTestCase { newSelection = new } + func onAddItemSelected(category: SecureVaultSorting.Category) {} + private func accounts(from sections: [PasswordManagementListSection]) -> [SecureVaultModels.WebsiteAccount] { var accounts = [SecureVaultModels.WebsiteAccount]() @@ -201,9 +203,11 @@ final class PasswordManagementItemListModelTests: XCTestCase { extension PasswordManagementItemListModel { - convenience init(onItemSelected: @escaping (_ old: SecureVaultItem?, _ new: SecureVaultItem?) -> Void) { + convenience init(onItemSelected: @escaping (_ old: SecureVaultItem?, _ new: SecureVaultItem?) -> Void, + onAddItemSelected: @escaping (_ category: SecureVaultSorting.Category) -> Void) { self.init(passwordManagerCoordinator: PasswordManagerCoordinatingMock(), - onItemSelected: onItemSelected) + onItemSelected: onItemSelected, + onAddItemSelected: onAddItemSelected) } }