-
Notifications
You must be signed in to change notification settings - Fork 425
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
Credential provider extension with credential list support #3658
Merged
amddg44
merged 24 commits into
feature/system-credential-provider
from
anya/credential-provider-extension
Dec 7, 2024
Merged
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
4fbcfbf
Autofill Credential Provider extension created
amddg44 ac00614
App groups & keychain sharing enabled on alpha debug variant
amddg44 2980291
Extension scheme created and set up pointing to Alpha Debug
amddg44 e941cb3
Keychain access group for secure vault added
amddg44 9b13cc8
Point to BSK commit
amddg44 8e8f29a
Update BSK reference
amddg44 24242c0
Merge remote-tracking branch 'origin/feature/system-credential-provid…
amddg44 26c4c71
Merge remote-tracking branch 'origin/feature/system-credential-provid…
amddg44 555417b
Update BSK reference
amddg44 c7eaa44
Alpha entitlements file cleanup, deleting the now unnecessary debug v…
amddg44 7d53652
Extension entitlements tidy up for Alpha / Alpha Debug target
amddg44 954d8ec
Get rid of storyboard + set up folder structure
amddg44 daf33df
Activation screen implemented
amddg44 d076424
Delete boilerplate sample code
amddg44 77a9c12
Refactored existing app code to move parts needed by credential exten…
amddg44 fa2c3c9
Support for credentials list with search, empty state, empty search s…
amddg44 3264c58
Entitlement updated for favicon access
amddg44 0637a25
Handling for when no device passcode / biometrics has been enabled
amddg44 d7ba9fd
Update tests
amddg44 c8a0f1b
Swiftlint updates
amddg44 51bdd5d
Fix filename with sprurious space character
amddg44 bb56fb3
Merge remote-tracking branch 'origin/feature/system-credential-provid…
amddg44 e29a8b4
Updated BSK commit
amddg44 6d05447
Addresses PR feedback
amddg44 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
AutofillCredentialProvider/AutofillCredentialProvider.entitlements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key> | ||
<false/> | ||
</dict> | ||
</plist> |
18 changes: 18 additions & 0 deletions
18
AutofillCredentialProvider/AutofillCredentialProviderAlpha.entitlements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key> | ||
<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> |
80 changes: 80 additions & 0 deletions
80
...der/CredentialProvider/CredentialProviderActivation/CredentialProviderActivatedView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// | ||
// CredentialProviderActivatedView.swift | ||
// DuckDuckGo | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import SwiftUI | ||
import DesignResourcesKit | ||
import DuckUI | ||
|
||
struct CredentialProviderActivatedView: View { | ||
|
||
let viewModel: CredentialProviderActivatedViewModel | ||
@State private var imageAppeared = false | ||
|
||
var body: some View { | ||
NavigationView { | ||
|
||
VStack(spacing: 0) { | ||
|
||
Image(.passwordsDDG96X96) | ||
.padding(.top, 48) | ||
.scaleEffect(imageAppeared ? 1 : 0.7) | ||
.animation( | ||
.interpolatingSpring(stiffness: 170, damping: 10) | ||
.delay(0.1), | ||
value: imageAppeared | ||
) | ||
.onAppear { | ||
imageAppeared = true | ||
} | ||
|
||
Text(UserText.credentialProviderActivatedTitle) | ||
.daxTitle2() | ||
.foregroundColor(Color(designSystemColor: .textPrimary)) | ||
.padding(.top, 16) | ||
.multilineTextAlignment(.center) | ||
|
||
Text(UserText.credentialProviderActivatedSubtitle) | ||
.daxBodyRegular() | ||
.foregroundColor(Color(designSystemColor: .textSecondary)) | ||
.padding(.top, 8) | ||
.multilineTextAlignment(.center) | ||
|
||
Spacer() | ||
|
||
Button { | ||
viewModel.launchDDGApp() | ||
} label: { | ||
Text(UserText.credentialProviderActivatedButton) | ||
} | ||
.buttonStyle(PrimaryButtonStyle()) | ||
.padding(.bottom, 12) | ||
|
||
} | ||
.padding(.horizontal, 24) | ||
.navigationBarItems(trailing: Button(UserText.actionCancel) { | ||
viewModel.dismiss() | ||
}) | ||
} | ||
} | ||
|
||
} | ||
|
||
#Preview { | ||
CredentialProviderActivatedView(viewModel: CredentialProviderActivatedViewModel()) | ||
} |
39 changes: 39 additions & 0 deletions
39
...redentialProvider/CredentialProviderActivation/CredentialProviderActivatedViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// CredentialProviderActivatedViewModel.swift | ||
// DuckDuckGo | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
|
||
struct CredentialProviderActivatedViewModel { | ||
|
||
typealias LaunchAppCompletion = (_ shouldLaunchApp: Bool) -> Void | ||
|
||
let completion: LaunchAppCompletion? | ||
|
||
init(completion: LaunchAppCompletion? = nil) { | ||
self.completion = completion | ||
} | ||
|
||
func dismiss() { | ||
completion?(false) | ||
} | ||
|
||
func launchDDGApp() { | ||
completion?(true) | ||
} | ||
} |
184 changes: 184 additions & 0 deletions
184
...r/CredentialProvider/CredentialProviderList/CredentialProviderListItemTableViewCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// | ||
// CredentialProviderListItemTableViewCell.swift | ||
// DuckDuckGo | ||
// | ||
// Copyright © 2024 DuckDuckGo. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import UIKit | ||
import Core | ||
import DesignResourcesKit | ||
|
||
class CredentialProviderListItemTableViewCell: UITableViewCell { | ||
|
||
static var reuseIdentifier = "CredentialProviderListItemTableViewCell" | ||
|
||
private lazy var titleLabel: UILabel = { | ||
let label = UILabel(frame: CGRect.zero) | ||
label.font = .preferredFont(forTextStyle: .callout) | ||
label.textColor = .init(designSystemColor: .textPrimary) | ||
label.lineBreakMode = .byTruncatingMiddle | ||
return label | ||
}() | ||
|
||
private lazy var subtitleLabel: UILabel = { | ||
let label = UILabel(frame: CGRect.zero) | ||
label.font = .preferredFont(forTextStyle: .footnote) | ||
label.textColor = .init(designSystemColor: .textPrimary) | ||
label.lineBreakMode = .byTruncatingMiddle | ||
return label | ||
}() | ||
|
||
private lazy var iconImageView: UIImageView = { | ||
let imageView = UIImageView() | ||
imageView.contentMode = .scaleAspectFill | ||
imageView.layer.masksToBounds = true | ||
imageView.layer.cornerRadius = 4 | ||
return imageView | ||
}() | ||
|
||
private lazy var textStackView: UIStackView = { | ||
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) | ||
stackView.axis = .vertical | ||
stackView.distribution = .fill | ||
stackView.spacing = 3 | ||
return stackView | ||
}() | ||
|
||
private lazy var contentStackView: UIStackView = { | ||
let stackView = UIStackView(arrangedSubviews: [iconImageView, textStackView]) | ||
stackView.axis = .horizontal | ||
stackView.spacing = 12 | ||
stackView.alignment = .center | ||
return stackView | ||
}() | ||
|
||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | ||
super.init(style: style, reuseIdentifier: reuseIdentifier) | ||
installSubviews() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
var item: AutofillLoginItem? { | ||
didSet { | ||
guard let item = item else { | ||
return | ||
} | ||
setupContentView(with: item) | ||
} | ||
} | ||
|
||
private func installSubviews() { | ||
contentView.addSubview(contentStackView) | ||
installConstraints() | ||
} | ||
|
||
private func installConstraints() { | ||
contentStackView.translatesAutoresizingMaskIntoConstraints = false | ||
iconImageView.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
let imageSize: CGFloat = 32 | ||
let margins = contentView.layoutMarginsGuide | ||
|
||
NSLayoutConstraint.activate([ | ||
iconImageView.widthAnchor.constraint(equalToConstant: imageSize), | ||
iconImageView.heightAnchor.constraint(equalToConstant: imageSize), | ||
|
||
contentStackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor), | ||
contentStackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor), | ||
contentStackView.topAnchor.constraint(equalTo: margins.topAnchor), | ||
contentStackView.bottomAnchor.constraint(equalTo: margins.bottomAnchor) | ||
]) | ||
} | ||
|
||
private func setupContentView(with item: AutofillLoginItem) { | ||
titleLabel.text = item.title | ||
subtitleLabel.text = item.subtitle | ||
iconImageView.image = loadImageFromCache(forDomain: item.account.domain) | ||
} | ||
|
||
override func layoutSubviews() { | ||
super.layoutSubviews() | ||
contentStackView.frame = contentView.bounds | ||
|
||
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 else { return nil } | ||
|
||
let key = FaviconHasher.createHash(ofDomain: domain) | ||
guard let cacheUrl = FaviconsCacheType.fireproof.cacheLocation() else { return nil } | ||
|
||
// Slight leap here to avoid loading Kingisher as a library for the widgets. | ||
// Once dependency management is fixed, link it and use Favicons directly. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏼 |
||
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) | ||
} | ||
|
||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Maybe could slightly restructure this as:
Just putting the two guards at the beginning.