Skip to content

Commit

Permalink
Enable System credential extension on release + translations (#3706)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1201645688934642/1207540610577903/f
Tech Design URL:
CC:

**Description**:
Enables the app to be a system level credential provider on the main app
target + translations and final ship review feedback
  • Loading branch information
amddg44 authored Dec 10, 2024
1 parent ac433ea commit 352d7bb
Show file tree
Hide file tree
Showing 86 changed files with 4,122 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
<plist version="1.0">
<dict>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<false/>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(GROUP_ID_PREFIX).vault</string>
<string>$(GROUP_ID_PREFIX).bookmarks</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(APP_ID)</string>
<string>$(AppIdentifierPrefix)$(VAULT_APP_GROUP)</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ struct CredentialProviderActivatedView: View {
.padding(.top, 16)
.multilineTextAlignment(.center)

Text(UserText.credentialProviderActivatedSubtitle)
.daxBodyRegular()
.foregroundColor(Color(designSystemColor: .textSecondary))
.padding(.top, 8)
.multilineTextAlignment(.center)

Spacer()

Button {
Expand All @@ -67,7 +61,7 @@ struct CredentialProviderActivatedView: View {

}
.padding(.horizontal, 24)
.navigationBarItems(trailing: Button(UserText.actionCancel) {
.navigationBarItems(trailing: Button(UserText.actionDone) {
viewModel.dismiss()
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//

import Foundation
import Core

struct CredentialProviderActivatedViewModel {

Expand All @@ -30,10 +31,12 @@ struct CredentialProviderActivatedViewModel {
}

func dismiss() {
Pixel.fire(pixel: .autofillExtensionWelcomeDismiss)
completion?(false)
}

func launchDDGApp() {
Pixel.fire(pixel: .autofillExtensionWelcomeLaunchApp)
completion?(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import DesignResourcesKit
class CredentialProviderListItemTableViewCell: UITableViewCell {

static var reuseIdentifier = "CredentialProviderListItemTableViewCell"


var disclosureButtonTapped: (() -> Void)?

private lazy var titleLabel: UILabel = {
let label = UILabel(frame: CGRect.zero)
label.font = .preferredFont(forTextStyle: .callout)
Expand Down Expand Up @@ -64,7 +66,23 @@ class CredentialProviderListItemTableViewCell: UITableViewCell {
stackView.alignment = .center
return stackView
}()


private lazy var disclosureButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(systemName: "chevron.forward")
let boldImage = image?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 11, weight: .bold))
button.setImage(boldImage, for: .normal)
button.tintColor = UIColor.tertiaryLabel
button.addTarget(self, action: #selector(handleDisclosureButtonTap), for: .touchUpInside)

let buttonSize: CGFloat = 44
button.frame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
button.contentHorizontalAlignment = .center
button.contentVerticalAlignment = .center

return button
}()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
installSubviews()
Expand All @@ -85,22 +103,29 @@ class CredentialProviderListItemTableViewCell: UITableViewCell {

private func installSubviews() {
contentView.addSubview(contentStackView)
contentView.addSubview(disclosureButton)
installConstraints()
}

private func installConstraints() {
contentStackView.translatesAutoresizingMaskIntoConstraints = false
iconImageView.translatesAutoresizingMaskIntoConstraints = false

disclosureButton.translatesAutoresizingMaskIntoConstraints = false

let imageSize: CGFloat = 32
let margins = contentView.layoutMarginsGuide

NSLayoutConstraint.activate([
iconImageView.widthAnchor.constraint(equalToConstant: imageSize),
iconImageView.heightAnchor.constraint(equalToConstant: imageSize),


disclosureButton.widthAnchor.constraint(equalToConstant: 44),
disclosureButton.heightAnchor.constraint(equalToConstant: 44),
disclosureButton.centerYAnchor.constraint(equalTo: margins.centerYAnchor),
disclosureButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 16),

contentStackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: disclosureButton.leadingAnchor, constant: -12),
contentStackView.topAnchor.constraint(equalTo: margins.topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: margins.bottomAnchor)
])
Expand All @@ -109,7 +134,7 @@ class CredentialProviderListItemTableViewCell: UITableViewCell {
private func setupContentView(with item: AutofillLoginItem) {
titleLabel.text = item.title
subtitleLabel.text = item.subtitle
iconImageView.image = loadImageFromCache(forDomain: item.account.domain)
iconImageView.image = FaviconHelper.loadImageFromCache(forDomain: item.account.domain, preferredFakeFaviconLetters: item.preferredFaviconLetters)
}

override func layoutSubviews() {
Expand All @@ -118,67 +143,9 @@ class CredentialProviderListItemTableViewCell: UITableViewCell {

separatorInset = UIEdgeInsets(top: 0, left: contentView.layoutMargins.left + textStackView.frame.origin.x, bottom: 0, right: 0)
}


private func loadImageFromCache(forDomain domain: String?) -> UIImage? {
guard let domain = domain,
let cacheUrl = FaviconsCacheType.fireproof.cacheLocation() else { return nil }

let key = FaviconHasher.createHash(ofDomain: domain)

// Slight leap here to avoid loading Kingisher as a library for the widgets.
// Once dependency management is fixed, link it and use Favicons directly.
let imageUrl = cacheUrl.appendingPathComponent("com.onevcat.Kingfisher.ImageCache.fireproof").appendingPathComponent(key)

guard let data = (try? Data(contentsOf: imageUrl)) else {
let image = createFakeFavicon(forDomain: domain, size: 32, backgroundColor: UIColor.forDomain(domain), preferredFakeFaviconLetters: item?.preferredFaviconLetters)
return image
}

return UIImage(data: data)?.toSRGB()
}

private func createFakeFavicon(forDomain domain: String,
size: CGFloat = 192,
backgroundColor: UIColor = UIColor.red,
bold: Bool = true,
preferredFakeFaviconLetters: String? = nil,
letterCount: Int = 2) -> UIImage? {

let cornerRadius = size * 0.125
let imageRect = CGRect(x: 0, y: 0, width: size, height: size)
let padding = size * 0.16
let labelFrame = CGRect(x: padding, y: padding, width: imageRect.width - (2 * padding), height: imageRect.height - (2 * padding))

let renderer = UIGraphicsImageRenderer(size: imageRect.size)
let icon = renderer.image { imageContext in
let context = imageContext.cgContext

context.setFillColor(backgroundColor.cgColor)
context.addPath(CGPath(roundedRect: imageRect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
context.fillPath()

let label = UILabel(frame: labelFrame)
label.numberOfLines = 1
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.1
label.baselineAdjustment = .alignCenters
label.font = bold ? UIFont.boldSystemFont(ofSize: size) : UIFont.systemFont(ofSize: size)
label.textColor = .white
label.textAlignment = .center

if let prefferedPrefix = preferredFakeFaviconLetters?.droppingWwwPrefix().prefix(letterCount).capitalized {
label.text = prefferedPrefix
} else {
label.text = item?.preferredFaviconLetters.capitalized ?? "#"
}

context.translateBy(x: padding, y: padding)

label.layer.draw(in: context)
}

return icon.withRenderingMode(.alwaysOriginal)
@objc private func handleDisclosureButtonTap() {
disclosureButtonTapped?()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ import UIKit
import AuthenticationServices
import BrowserServicesKit
import Combine
import Common
import Core
import SwiftUI

final class CredentialProviderListViewController: UIViewController {

private let viewModel: CredentialProviderListViewModel
private let shouldProvideTextToInsert: Bool
private let tld: TLD
private let onRowSelected: (AutofillLoginItem) -> Void
private let onTextProvided: (String) -> Void
private let onDismiss: () -> Void
private var cancellables: Set<AnyCancellable> = []

Expand Down Expand Up @@ -80,21 +84,35 @@ final class CredentialProviderListViewController: UIViewController {
constant: (tableView.frame.height / 2))
}()


init(serviceIdentifiers: [ASCredentialServiceIdentifier],
secureVault: (any AutofillSecureVault)?,
credentialIdentityStoreManager: AutofillCredentialIdentityStoreManaging,
shouldProvideTextToInsert: Bool,
tld: TLD,
onRowSelected: @escaping (AutofillLoginItem) -> Void,
onTextProvided: @escaping (String) -> Void,
onDismiss: @escaping () -> Void) {
self.viewModel = CredentialProviderListViewModel(serviceIdentifiers: serviceIdentifiers,
secureVault: secureVault,
credentialIdentityStoreManager: credentialIdentityStoreManager)
credentialIdentityStoreManager: credentialIdentityStoreManager,
tld: tld)
self.shouldProvideTextToInsert = shouldProvideTextToInsert
self.tld = tld
self.onRowSelected = onRowSelected
self.onTextProvided = onTextProvided
self.onDismiss = onDismiss

super.init(nibName: nil, bundle: nil)

authenticate()
if #available(iOS 18.0, *) {
authenticate()
} else {
// pre-iOS 18.0 authentication can fail silently if extension is loaded twice in quick succession
// if authenticate is called without a slight delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.authenticate()
}
}
}

required init?(coder: NSCoder) {
Expand All @@ -106,6 +124,10 @@ final class CredentialProviderListViewController: UIViewController {

title = UserText.credentialProviderListTitle

if let itemPrompt = viewModel.serviceIdentifierPromptLabel {
navigationItem.prompt = itemPrompt
}

let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
navigationItem.rightBarButtonItem = doneItem

Expand All @@ -117,6 +139,8 @@ final class CredentialProviderListViewController: UIViewController {
registerForKeyboardNotifications()

navigationItem.searchController = searchController

Pixel.fire(pixel: .autofillExtensionPasswordsOpened)
}

override func viewWillDisappear(_ animated: Bool) {
Expand Down Expand Up @@ -156,19 +180,17 @@ final class CredentialProviderListViewController: UIViewController {
}

private func authenticate() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.viewModel.authenticate {[weak self] error in
guard let self = self else { return }

if error != nil {
if error != .noAuthAvailable {
self.onDismiss()
} else {
let alert = UIAlertController.makeDeviceAuthenticationAlert { [weak self] in
self?.onDismiss()
}
present(alert, animated: true)
viewModel.authenticate {[weak self] error in
guard let self = self else { return }

if error != nil {
if error != .noAuthAvailable {
self.onDismiss()
} else {
let alert = UIAlertController.makeDeviceAuthenticationAlert { [weak self] in
self?.onDismiss()
}
present(alert, animated: true)
}
}
}
Expand Down Expand Up @@ -270,6 +292,7 @@ final class CredentialProviderListViewController: UIViewController {

@objc private func doneTapped() {
onDismiss()
Pixel.fire(pixel: .autofillExtensionPasswordsDismissed)
}

}
Expand All @@ -293,6 +316,11 @@ extension CredentialProviderListViewController: UITableViewDataSource {
}
cell.item = items[indexPath.row]
cell.backgroundColor = UIColor(designSystemColor: .surface)

cell.disclosureButtonTapped = { [weak self] in
let item = items[indexPath.row]
self?.presentDetailsForCredentials(item: item)
}
return cell
default:
return UITableViewCell()
Expand All @@ -312,6 +340,14 @@ extension CredentialProviderListViewController: UITableViewDataSource {
viewModel.viewState == .showItems ? UILocalizedIndexedCollation.current().sectionIndexTitles : []
}

private func presentDetailsForCredentials(item: AutofillLoginItem) {
let detailViewController = CredentialProviderListDetailsViewController(account: item.account,
tld: tld,
shouldProvideTextToInsert: self.shouldProvideTextToInsert)
detailViewController.delegate = self

self.navigationController?.pushViewController(detailViewController, animated: true)
}
}

extension CredentialProviderListViewController: UITableViewDelegate {
Expand All @@ -321,9 +357,14 @@ extension CredentialProviderListViewController: UITableViewDelegate {
switch viewModel.sections[indexPath.section] {
case .suggestions(_, items: let items), .credentials(_, let items):
let item = items[indexPath.row]
onRowSelected(item)
if shouldProvideTextToInsert {
presentDetailsForCredentials(item: item)
} else {
onRowSelected(item)
Pixel.fire(pixel: .autofillExtensionPasswordSelected)
}
default:
break
return
}
}

Expand Down Expand Up @@ -378,5 +419,12 @@ extension CredentialProviderListViewController {
(tableView.frame.height / 2) - searchController.searchBar.frame.height
)
}
}

extension CredentialProviderListViewController: CredentialProviderListDetailsViewControllerDelegate {

func credentialProviderListDetailsViewControllerDidProvideText(_ controller: CredentialProviderListDetailsViewController, text: String) {
onTextProvided(text)
}

}
Loading

0 comments on commit 352d7bb

Please sign in to comment.