diff --git a/App/Info.plist b/App/Info.plist
index 9824fb9c3..76c53c723 100644
--- a/App/Info.plist
+++ b/App/Info.plist
@@ -33,6 +33,10 @@
CFBundleVersion
1
+ LSApplicationQueriesSchemes
+
+ org-appextension-feature-password-management
+
LSRequiresIPhoneOS
UILaunchStoryboardName
diff --git a/App/ViewController.swift b/App/ViewController.swift
index 6f59973a3..75f8a9734 100644
--- a/App/ViewController.swift
+++ b/App/ViewController.swift
@@ -50,6 +50,8 @@ class ViewController: UIViewController {
.classic()
.withOptions {
applyDefaultOptions(&$0)
+ $0.passwordManager.appIdentifier = "www.myapp.com"
+ $0.passwordManager.displayName = "My App"
$0.customSignupFields = [
CustomTextField(name: "first_name", placeholder: "First Name", icon: LazyImage(name: "ic_person", bundle: Lock.bundle)),
CustomTextField(name: "last_name", placeholder: "Last Name", icon: LazyImage(name: "ic_person", bundle: Lock.bundle))
@@ -255,6 +257,9 @@ func applyPhantomStyle(_ style: inout Style) {
// Table View
style.searchBarStyle = .minimal
+
+ // 1 Password
+ style.onePasswordIconColor = darkPurple
}
class CleanroomLockLogger: LoggerOutput {
diff --git a/Lock.podspec b/Lock.podspec
index 2c63e7f0f..1828a4a77 100644
--- a/Lock.podspec
+++ b/Lock.podspec
@@ -23,7 +23,7 @@ Auth0 is a SaaS that helps you with Authentication and Authorization. You can us
s.default_subspecs = 'Classic'
s.subspec 'Classic' do |classic|
- classic.ios.source_files = "Lock/**/*.swift"
+ classic.ios.source_files = "Lock/**/*.{swift,h,m}"
classic.ios.resource = ["Lock/*.xcassets", "Lock/*.lproj", "Lock/passwordless_country_codes.plist"]
end
diff --git a/Lock.xcodeproj/project.pbxproj b/Lock.xcodeproj/project.pbxproj
index cdfe34ef4..605ebcb4a 100644
--- a/Lock.xcodeproj/project.pbxproj
+++ b/Lock.xcodeproj/project.pbxproj
@@ -14,8 +14,6 @@
5B0CF2D61DE9AE0F00F82BF4 /* InputValidationErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0CF2D41DE9ADF900F82BF4 /* InputValidationErrorSpec.swift */; };
5B0CF2D91DE9B1DB00F82BF4 /* DatabaseAuthenticatableErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0CF2D71DE9B14000F82BF4 /* DatabaseAuthenticatableErrorSpec.swift */; };
5B0CF2DE1DE9B4AF00F82BF4 /* DatabaseUserCreatorErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0CF2DC1DE9B47C00F82BF4 /* DatabaseUserCreatorErrorSpec.swift */; };
- 5B124ED01EA8D15E0050E567 /* LockSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B124ECF1EA8D15E0050E567 /* LockSnapshot.swift */; };
- 5B124ED81EA8D17A0050E567 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B124ED71EA8D17A0050E567 /* SnapshotHelper.swift */; };
5B1FD96D1E4E2B6B0055C1AC /* PasswordlessActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD96C1E4E2B6B0055C1AC /* PasswordlessActivity.swift */; };
5B1FD9701E4E2F170055C1AC /* PasswordlessActivitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD96E1E4E2E670055C1AC /* PasswordlessActivitySpec.swift */; };
5B1FD9721E546F500055C1AC /* PasswordlessInteractorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1FD9711E546F500055C1AC /* PasswordlessInteractorSpec.swift */; };
@@ -26,7 +24,10 @@
5B3440C51E7C09A1009F8BF7 /* PasswordlessRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3440C41E7C09A1009F8BF7 /* PasswordlessRouter.swift */; };
5B3440CB1E7C11E7009F8BF7 /* ClassicRouterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3440C91E7C11E5009F8BF7 /* ClassicRouterSpec.swift */; };
5B3440CC1E7C11EB009F8BF7 /* PasswordlessRouterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3440C61E7C0D6E009F8BF7 /* PasswordlessRouterSpec.swift */; };
- 5B3DA2531E6DE88900370C17 /* InternationalPhoneInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3DA2521E6DE88900370C17 /* InternationalPhoneInputView.swift */; };
+ 5B3874CA1E97C0C600244326 /* InternationalPhoneInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3874C91E97C0C600244326 /* InternationalPhoneInputView.swift */; };
+ 5B3874D41E97C13D00244326 /* CountryCodesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3874D01E97C11000244326 /* CountryCodesSpec.swift */; };
+ 5B3874D51E97C14000244326 /* PolicyViewSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3874D21E97C11E00244326 /* PolicyViewSpec.swift */; };
+ 5B38F84F1E93F2A300C25EAC /* OnePasswordSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B38F84D1E93F2A000C25EAC /* OnePasswordSpec.swift */; };
5B3DA2551E6EB9F800370C17 /* CountryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3DA2541E6EB9F800370C17 /* CountryTableViewController.swift */; };
5B4BA3EB1E8000100067FC3F /* Auth0OAuth2InteractorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F92C68A1D4FE90F00CCE6C0 /* Auth0OAuth2InteractorSpec.swift */; };
5B4BA3EC1E8000170067FC3F /* EnterpriseDomainInteractorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6631511DDB990B001CB043 /* EnterpriseDomainInteractorSpec.swift */; };
@@ -48,17 +49,22 @@
5B9A54461E4B2CB4004B5454 /* PasswordlessAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9A54451E4B2CB4004B5454 /* PasswordlessAuthenticatable.swift */; };
5B9A544A1E4B354A004B5454 /* PasswordlessPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9A54491E4B354A004B5454 /* PasswordlessPresenter.swift */; };
5BA563F11DD117550002D3AB /* EnterpriseActiveAuthInteractorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA563EF1DD1171F0002D3AB /* EnterpriseActiveAuthInteractorSpec.swift */; };
+ 5BB142EE1E97C2F000315607 /* CountryTableViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB142EB1E97C2E000315607 /* CountryTableViewControllerSpec.swift */; };
+ 5BB142EF1E97C2F300315607 /* LockViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB142EA1E97C2E000315607 /* LockViewControllerSpec.swift */; };
5BB4A7C11DF9A38E008E8C37 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB4A7C01DF9A38E008E8C37 /* DatabaseView.swift */; };
5BB78CC21E54BC730074DFE3 /* PasswordlessAuthenticatableErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB78CC01E54BC2D0074DFE3 /* PasswordlessAuthenticatableErrorSpec.swift */; };
5BB78CC51E54BD530074DFE3 /* PasswordlessPresenterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB78CC31E54BD440074DFE3 /* PasswordlessPresenterSpec.swift */; };
- 5BBEED381E79A9F900B3308D /* CountryCodesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBEED361E79A96C00B3308D /* CountryCodesSpec.swift */; };
- 5BCB26861E8437BC00CFBF9A /* PolicyViewSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCB26851E8437BC00CFBF9A /* PolicyViewSpec.swift */; };
+ 5BBD9E311E8E6F5E007EF625 /* OnePasswordExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BBD9E2F1E8E6F5E007EF625 /* OnePasswordExtension.m */; };
+ 5BBD9E321E8E6F5E007EF625 /* OnePasswordExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = 5BBD9E301E8E6F5E007EF625 /* OnePasswordExtension.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 5BBD9E361E8E730C007EF625 /* OnePassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBD9E351E8E730C007EF625 /* OnePassword.swift */; };
+ 5BBD9E3A1E8E929E007EF625 /* IconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBD9E391E8E929E007EF625 /* IconButton.swift */; };
5BCDE1361DDDF17F00AA2A6C /* EnterpriseActiveAuthPresenterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCDE1341DDDF12100AA2A6C /* EnterpriseActiveAuthPresenterSpec.swift */; };
5BCED4C71DD1FEAA00E2CE8A /* EnterpriseDomainPresenterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCED4C51DD1FCF200E2CE8A /* EnterpriseDomainPresenterSpec.swift */; };
5BE4344E1E64820800950FA1 /* HRDAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE4344C1E64820800950FA1 /* HRDAuthenticatable.swift */; };
5BE4344F1E64820800950FA1 /* PasswordRecoverable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE4344D1E64820800950FA1 /* PasswordRecoverable.swift */; };
- 5BE978201E82E7FA0090EC07 /* CountryTableViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE9781F1E82E7FA0090EC07 /* CountryTableViewControllerSpec.swift */; };
5BE978241E8405C70090EC07 /* PasswordlessAuthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE978231E8405C70090EC07 /* PasswordlessAuthTransaction.swift */; };
+ 5BEDE14A1EC0A9F10007300D /* LockUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1441EC0A9EB0007300D /* LockUITests.swift */; };
+ 5BEDE14C1EC0A9F10007300D /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1461EC0A9EB0007300D /* SnapshotHelper.swift */; };
5F0FCF901E20117E00E3D53B /* ObserverStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F0FCF8F1E20117E00E3D53B /* ObserverStore.swift */; };
5F0FCF921E201CF300E3D53B /* ObserverStoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F0FCF911E201CF300E3D53B /* ObserverStoreSpec.swift */; };
5F14565A1D5130E80085DF9C /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1456591D5130E80085DF9C /* Colors.swift */; };
@@ -69,7 +75,6 @@
5F1C49971D8360F5005B74FC /* CDNLoaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1C49951D8360F5005B74FC /* CDNLoaderInteractor.swift */; };
5F1C499B1D836190005B74FC /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F1C499A1D836190005B74FC /* CustomTextField.swift */; };
5F2037C21D5D02880005D2E2 /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2037C11D5D02880005D2E2 /* Matchers.swift */; };
- 5F2496AF1D66210500A1C6E2 /* LockViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496AE1D66210500A1C6E2 /* LockViewControllerSpec.swift */; };
5F2496B31D665A5600A1C6E2 /* DatabaseUserCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496B21D665A5600A1C6E2 /* DatabaseUserCreator.swift */; };
5F2496B61D665AA800A1C6E2 /* InputValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496B51D665AA800A1C6E2 /* InputValidationError.swift */; };
5F2496B81D665AC500A1C6E2 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496B71D665AC500A1C6E2 /* UserAttribute.swift */; };
@@ -180,7 +185,7 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
- 5B124ED21EA8D15E0050E567 /* PBXContainerItemProxy */ = {
+ 5BEDE13F1EC0A9750007300D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5FEAE1BD1D1A5154005C0028 /* Project object */;
proxyType = 1;
@@ -236,10 +241,6 @@
5B0CF2D41DE9ADF900F82BF4 /* InputValidationErrorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputValidationErrorSpec.swift; sourceTree = ""; };
5B0CF2D71DE9B14000F82BF4 /* DatabaseAuthenticatableErrorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAuthenticatableErrorSpec.swift; sourceTree = ""; };
5B0CF2DC1DE9B47C00F82BF4 /* DatabaseUserCreatorErrorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUserCreatorErrorSpec.swift; sourceTree = ""; };
- 5B124ECD1EA8D15D0050E567 /* LockSnapshot.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LockSnapshot.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 5B124ECF1EA8D15E0050E567 /* LockSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockSnapshot.swift; sourceTree = ""; };
- 5B124ED11EA8D15E0050E567 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 5B124ED71EA8D17A0050E567 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; };
5B1FD96C1E4E2B6B0055C1AC /* PasswordlessActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessActivity.swift; sourceTree = ""; };
5B1FD96E1E4E2E670055C1AC /* PasswordlessActivitySpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessActivitySpec.swift; sourceTree = ""; };
5B1FD9711E546F500055C1AC /* PasswordlessInteractorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessInteractorSpec.swift; sourceTree = ""; };
@@ -250,7 +251,10 @@
5B3440C41E7C09A1009F8BF7 /* PasswordlessRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessRouter.swift; sourceTree = ""; };
5B3440C61E7C0D6E009F8BF7 /* PasswordlessRouterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessRouterSpec.swift; sourceTree = ""; };
5B3440C91E7C11E5009F8BF7 /* ClassicRouterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassicRouterSpec.swift; sourceTree = ""; };
- 5B3DA2521E6DE88900370C17 /* InternationalPhoneInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternationalPhoneInputView.swift; sourceTree = ""; };
+ 5B3874C91E97C0C600244326 /* InternationalPhoneInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternationalPhoneInputView.swift; sourceTree = ""; };
+ 5B3874D01E97C11000244326 /* CountryCodesSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryCodesSpec.swift; sourceTree = ""; };
+ 5B3874D21E97C11E00244326 /* PolicyViewSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolicyViewSpec.swift; sourceTree = ""; };
+ 5B38F84D1E93F2A000C25EAC /* OnePasswordSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnePasswordSpec.swift; sourceTree = ""; };
5B3DA2541E6EB9F800370C17 /* CountryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryTableViewController.swift; sourceTree = ""; };
5B4DE0141DD66DE1004C8AC2 /* EnterpriseDomainInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseDomainInteractor.swift; path = Lock/EnterpriseDomainInteractor.swift; sourceTree = SOURCE_ROOT; };
5B4DE0161DD67064004C8AC2 /* EnterpriseActiveAuthPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnterpriseActiveAuthPresenter.swift; path = Lock/EnterpriseActiveAuthPresenter.swift; sourceTree = SOURCE_ROOT; };
@@ -271,17 +275,24 @@
5B9A54451E4B2CB4004B5454 /* PasswordlessAuthenticatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessAuthenticatable.swift; sourceTree = ""; };
5B9A54491E4B354A004B5454 /* PasswordlessPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessPresenter.swift; sourceTree = ""; };
5BA563EF1DD1171F0002D3AB /* EnterpriseActiveAuthInteractorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterpriseActiveAuthInteractorSpec.swift; sourceTree = ""; };
+ 5BB142EA1E97C2E000315607 /* LockViewControllerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockViewControllerSpec.swift; sourceTree = ""; };
+ 5BB142EB1E97C2E000315607 /* CountryTableViewControllerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryTableViewControllerSpec.swift; sourceTree = ""; };
5BB4A7C01DF9A38E008E8C37 /* DatabaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; };
5BB78CC01E54BC2D0074DFE3 /* PasswordlessAuthenticatableErrorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessAuthenticatableErrorSpec.swift; sourceTree = ""; };
5BB78CC31E54BD440074DFE3 /* PasswordlessPresenterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessPresenterSpec.swift; sourceTree = ""; };
- 5BBEED361E79A96C00B3308D /* CountryCodesSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryCodesSpec.swift; sourceTree = ""; };
- 5BCB26851E8437BC00CFBF9A /* PolicyViewSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolicyViewSpec.swift; sourceTree = ""; };
+ 5BBD9E2F1E8E6F5E007EF625 /* OnePasswordExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnePasswordExtension.m; sourceTree = ""; };
+ 5BBD9E301E8E6F5E007EF625 /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnePasswordExtension.h; sourceTree = ""; };
+ 5BBD9E351E8E730C007EF625 /* OnePassword.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnePassword.swift; sourceTree = ""; };
+ 5BBD9E391E8E929E007EF625 /* IconButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IconButton.swift; sourceTree = ""; };
5BCDE1341DDDF12100AA2A6C /* EnterpriseActiveAuthPresenterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterpriseActiveAuthPresenterSpec.swift; sourceTree = ""; };
5BCED4C51DD1FCF200E2CE8A /* EnterpriseDomainPresenterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterpriseDomainPresenterSpec.swift; sourceTree = ""; };
5BE4344C1E64820800950FA1 /* HRDAuthenticatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HRDAuthenticatable.swift; sourceTree = ""; };
5BE4344D1E64820800950FA1 /* PasswordRecoverable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordRecoverable.swift; sourceTree = ""; };
- 5BE9781F1E82E7FA0090EC07 /* CountryTableViewControllerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryTableViewControllerSpec.swift; sourceTree = ""; };
5BE978231E8405C70090EC07 /* PasswordlessAuthTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordlessAuthTransaction.swift; sourceTree = ""; };
+ 5BEDE13A1EC0A9750007300D /* LockUITests.iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LockUITests.iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 5BEDE13E1EC0A9750007300D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 5BEDE1441EC0A9EB0007300D /* LockUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockUITests.swift; sourceTree = ""; };
+ 5BEDE1461EC0A9EB0007300D /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; };
5F0FCF8F1E20117E00E3D53B /* ObserverStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverStore.swift; sourceTree = ""; };
5F0FCF911E201CF300E3D53B /* ObserverStoreSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ObserverStoreSpec.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
5F1456591D5130E80085DF9C /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Colors.swift; path = Lock/Colors.swift; sourceTree = SOURCE_ROOT; };
@@ -292,7 +303,6 @@
5F1C49951D8360F5005B74FC /* CDNLoaderInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CDNLoaderInteractor.swift; path = Lock/CDNLoaderInteractor.swift; sourceTree = SOURCE_ROOT; };
5F1C499A1D836190005B74FC /* CustomTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CustomTextField.swift; path = Lock/CustomTextField.swift; sourceTree = SOURCE_ROOT; };
5F2037C11D5D02880005D2E2 /* Matchers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = ""; };
- 5F2496AE1D66210500A1C6E2 /* LockViewControllerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockViewControllerSpec.swift; sourceTree = ""; };
5F2496B21D665A5600A1C6E2 /* DatabaseUserCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseUserCreator.swift; path = Lock/DatabaseUserCreator.swift; sourceTree = SOURCE_ROOT; };
5F2496B51D665AA800A1C6E2 /* InputValidationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InputValidationError.swift; path = Lock/InputValidationError.swift; sourceTree = SOURCE_ROOT; };
5F2496B71D665AC500A1C6E2 /* UserAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserAttribute.swift; path = Lock/UserAttribute.swift; sourceTree = SOURCE_ROOT; };
@@ -405,7 +415,7 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
- 5B124ECA1EA8D15D0050E567 /* Frameworks */ = {
+ 5BEDE1371EC0A9750007300D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -463,23 +473,41 @@
name = Validators;
sourceTree = "";
};
- 5B124ECE1EA8D15E0050E567 /* LockSnapshot */ = {
+ 5B3874CF1E97C0F100244326 /* Controllers */ = {
isa = PBXGroup;
children = (
- 5B124ED71EA8D17A0050E567 /* SnapshotHelper.swift */,
- 5B124ECF1EA8D15E0050E567 /* LockSnapshot.swift */,
- 5B124ED11EA8D15E0050E567 /* Info.plist */,
+ 5BB142EA1E97C2E000315607 /* LockViewControllerSpec.swift */,
+ 5BB142EB1E97C2E000315607 /* CountryTableViewControllerSpec.swift */,
);
- path = LockSnapshot;
+ name = Controllers;
sourceTree = "";
};
- 5BE978191E82CFA00090EC07 /* Controllers */ = {
+ 5B38F8501E93F45C00C25EAC /* Extension */ = {
isa = PBXGroup;
children = (
- 5F2496AE1D66210500A1C6E2 /* LockViewControllerSpec.swift */,
- 5BE9781F1E82E7FA0090EC07 /* CountryTableViewControllerSpec.swift */,
+ 5B38F84D1E93F2A000C25EAC /* OnePasswordSpec.swift */,
);
- name = Controllers;
+ name = Extension;
+ sourceTree = "";
+ };
+ 5B94D5AC1E8E6AF400E9D4F1 /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 5BBD9E351E8E730C007EF625 /* OnePassword.swift */,
+ 5BBD9E2F1E8E6F5E007EF625 /* OnePasswordExtension.m */,
+ 5BBD9E301E8E6F5E007EF625 /* OnePasswordExtension.h */,
+ );
+ name = Extensions;
+ sourceTree = "";
+ };
+ 5BEDE13B1EC0A9750007300D /* LockUITests */ = {
+ isa = PBXGroup;
+ children = (
+ 5BEDE1441EC0A9EB0007300D /* LockUITests.swift */,
+ 5BEDE1461EC0A9EB0007300D /* SnapshotHelper.swift */,
+ 5BEDE13E1EC0A9750007300D /* Info.plist */,
+ );
+ path = LockUITests;
sourceTree = "";
};
5F2496B41D665A9700A1C6E2 /* Errors */ = {
@@ -632,7 +660,6 @@
isa = PBXGroup;
children = (
5F9231D41D5B6C5E00D92580 /* AuthCollectionViewSpec.swift */,
- 5BCB26851E8437BC00CFBF9A /* PolicyViewSpec.swift */,
);
name = Views;
sourceTree = "";
@@ -680,8 +707,8 @@
5FBE5CCB1D3ED9210038536D /* Models */ = {
isa = PBXGroup;
children = (
+ 5B3874D01E97C11000244326 /* CountryCodesSpec.swift */,
5FBE5CCC1D3EDF960038536D /* UserSpec.swift */,
- 5BBEED361E79A96C00B3308D /* CountryCodesSpec.swift */,
5F92C68C1D50E47100CCE6C0 /* AuthStyleSpec.swift */,
5FE50DC01D7DED8C00D82290 /* OfflineConnectionsSpec.swift */,
5FEEE8191DB6AF3800B4DFED /* RuleSpec.swift */,
@@ -731,7 +758,7 @@
5FEAE1C81D1A5154005C0028 /* Lock */,
5FEAE1D41D1A5154005C0028 /* LockTests */,
5FEADCF51D1A7EBC0032D810 /* App */,
- 5B124ECE1EA8D15E0050E567 /* LockSnapshot */,
+ 5BEDE13B1EC0A9750007300D /* LockUITests */,
5FEAE2111D1A5716005C0028 /* Frameworks */,
5FEAE1C71D1A5154005C0028 /* Products */,
);
@@ -743,7 +770,7 @@
5FEAE1C61D1A5154005C0028 /* Lock.framework */,
5FEAE1D01D1A5154005C0028 /* LockTests.iOS.xctest */,
5FEADCF41D1A7EBC0032D810 /* LockApp.app */,
- 5B124ECD1EA8D15D0050E567 /* LockSnapshot.xctest */,
+ 5BEDE13A1EC0A9750007300D /* LockUITests.iOS.xctest */,
);
name = Products;
sourceTree = "";
@@ -758,6 +785,7 @@
5FBE5CB51D3D8EE70038536D /* Models */,
5FC434871D1DF7FA005188BC /* Presenters */,
5FFC54FC1D37E3E400579581 /* Router */,
+ 5B94D5AC1E8E6AF400E9D4F1 /* Extensions */,
5F99AA801D1B0A2B00D27842 /* Utils */,
5F50900C1D1DF3ED00EAA650 /* Views */,
5F99AA831D1B0BCE00D27842 /* Lock.swift */,
@@ -775,13 +803,15 @@
5F5F98D11D21E3710016FC22 /* Presenters */,
5FFC54FF1D3803EC00579581 /* Router */,
5F5090061D1DE7A000EAA650 /* Utils */,
+ 5B3874CF1E97C0F100244326 /* Controllers */,
+ 5B38F8501E93F45C00C25EAC /* Extension */,
5FEAE1D71D1A5154005C0028 /* Info.plist */,
+ 5B3874D21E97C11E00244326 /* PolicyViewSpec.swift */,
5FA2504F1D48E2A200C544FA /* OptionsSpec.swift */,
5B1FD96E1E4E2E670055C1AC /* PasswordlessActivitySpec.swift */,
5FA250511D48F08200C544FA /* LockSpec.swift */,
5F92C6901D510AFE00CCE6C0 /* LazyImageSpec.swift */,
5F390E8C1D63B99300FC549C /* LoggerSpec.swift */,
- 5BE978191E82CFA00090EC07 /* Controllers */,
5B0CF2DB1DE9B3D300F82BF4 /* Validators */,
5B0CF2DA1DE9B3C400F82BF4 /* Errors */,
5F2496BD1D67ADB300A1C6E2 /* EmailValidatorSpec.swift */,
@@ -795,6 +825,7 @@
5FEAE20E1D1A5682005C0028 /* Components */ = {
isa = PBXGroup;
children = (
+ 5B3874C91E97C0C600244326 /* InternationalPhoneInputView.swift */,
5B2981771DD51A460062535C /* InfoBarView.swift */,
5F99AA951D1C4AF400D27842 /* CredentialView.swift */,
5F99AA8F1D1B9E7100D27842 /* DatabaseModeSwitcher.swift */,
@@ -804,11 +835,11 @@
5FDB41CF1D2C95B100166B67 /* MessageView.swift */,
5F99AA891D1B360C00D27842 /* PrimaryButton.swift */,
5F99AA931D1BABFC00D27842 /* SecondaryButton.swift */,
+ 5BBD9E391E8E929E007EF625 /* IconButton.swift */,
5F51EE671D1C88FC0024BCD6 /* SignUpView.swift */,
5F51EE691D1CBC830024BCD6 /* SingleInputView.swift */,
5FD6772B1D4C303C004B87C4 /* AuthButton.swift */,
5F5D4A251DC3FB34002A38EB /* PolicyView.swift */,
- 5B3DA2521E6DE88900370C17 /* InternationalPhoneInputView.swift */,
);
name = Components;
path = Lock;
@@ -866,6 +897,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
+ 5BBD9E321E8E6F5E007EF625 /* OnePasswordExtension.h in Headers */,
5FEAE1CA1D1A5154005C0028 /* Lock.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -873,22 +905,22 @@
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
- 5B124ECC1EA8D15D0050E567 /* LockSnapshot */ = {
+ 5BEDE1391EC0A9750007300D /* LockUITests.iOS */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 5B124ED61EA8D15E0050E567 /* Build configuration list for PBXNativeTarget "LockSnapshot" */;
+ buildConfigurationList = 5BEDE1411EC0A9750007300D /* Build configuration list for PBXNativeTarget "LockUITests.iOS" */;
buildPhases = (
- 5B124EC91EA8D15D0050E567 /* Sources */,
- 5B124ECA1EA8D15D0050E567 /* Frameworks */,
- 5B124ECB1EA8D15D0050E567 /* Resources */,
+ 5BEDE1361EC0A9750007300D /* Sources */,
+ 5BEDE1371EC0A9750007300D /* Frameworks */,
+ 5BEDE1381EC0A9750007300D /* Resources */,
);
buildRules = (
);
dependencies = (
- 5B124ED31EA8D15E0050E567 /* PBXTargetDependency */,
+ 5BEDE1401EC0A9750007300D /* PBXTargetDependency */,
);
- name = LockSnapshot;
- productName = LockSnapshot;
- productReference = 5B124ECD1EA8D15D0050E567 /* LockSnapshot.xctest */;
+ name = LockUITests.iOS;
+ productName = LockUITests;
+ productReference = 5BEDE13A1EC0A9750007300D /* LockUITests.iOS.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
5FEADCF31D1A7EBC0032D810 /* LockApp */ = {
@@ -960,9 +992,9 @@
LastUpgradeCheck = 0800;
ORGANIZATIONNAME = Auth0;
TargetAttributes = {
- 5B124ECC1EA8D15D0050E567 = {
- CreatedOnToolsVersion = 8.3;
- ProvisioningStyle = Manual;
+ 5BEDE1391EC0A9750007300D = {
+ CreatedOnToolsVersion = 8.3.2;
+ ProvisioningStyle = Automatic;
TestTargetID = 5FEADCF31D1A7EBC0032D810;
};
5FEADCF31D1A7EBC0032D810 = {
@@ -978,7 +1010,7 @@
};
5FEAE1C51D1A5154005C0028 = {
CreatedOnToolsVersion = 7.3.1;
- LastSwiftMigration = 0810;
+ LastSwiftMigration = 0820;
};
5FEAE1CF1D1A5154005C0028 = {
CreatedOnToolsVersion = 7.3.1;
@@ -1004,13 +1036,13 @@
5FEAE1C51D1A5154005C0028 /* Lock.iOS */,
5FEAE1CF1D1A5154005C0028 /* LockTests.iOS */,
5FEADCF31D1A7EBC0032D810 /* LockApp */,
- 5B124ECC1EA8D15D0050E567 /* LockSnapshot */,
+ 5BEDE1391EC0A9750007300D /* LockUITests.iOS */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
- 5B124ECB1EA8D15D0050E567 /* Resources */ = {
+ 5BEDE1381EC0A9750007300D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -1094,12 +1126,12 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- 5B124EC91EA8D15D0050E567 /* Sources */ = {
+ 5BEDE1361EC0A9750007300D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5B124ED01EA8D15E0050E567 /* LockSnapshot.swift in Sources */,
- 5B124ED81EA8D17A0050E567 /* SnapshotHelper.swift in Sources */,
+ 5BEDE14A1EC0A9F10007300D /* LockUITests.swift in Sources */,
+ 5BEDE14C1EC0A9F10007300D /* SnapshotHelper.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1142,6 +1174,7 @@
5B3DA2551E6EB9F800370C17 /* CountryTableViewController.swift in Sources */,
5F70F1DF1D7904A3004698DA /* ConnectionBuildable.swift in Sources */,
5FC4348F1D1E0A57005188BC /* DatabaseAuthenticable.swift in Sources */,
+ 5BBD9E361E8E730C007EF625 /* OnePassword.swift in Sources */,
5BE4344F1E64820800950FA1 /* PasswordRecoverable.swift in Sources */,
5FD6772C1D4C303C004B87C4 /* AuthButton.swift in Sources */,
5F99AA8A1D1B360C00D27842 /* PrimaryButton.swift in Sources */,
@@ -1165,6 +1198,7 @@
5B9A54441E4B275E004B5454 /* PasswordlessInteractor.swift in Sources */,
5F51EE681D1C88FC0024BCD6 /* SignUpView.swift in Sources */,
5F57DFD41D4FE64700C54DA8 /* Auth0OAuth2Interactor.swift in Sources */,
+ 5BBD9E3A1E8E929E007EF625 /* IconButton.swift in Sources */,
5B4DE0151DD66DE1004C8AC2 /* EnterpriseDomainInteractor.swift in Sources */,
5F57DFD21D4FE59800C54DA8 /* OAuth2Authenticatable.swift in Sources */,
5F508FF71D1D868900EAA650 /* DatabaseInteractor.swift in Sources */,
@@ -1176,12 +1210,12 @@
5B3440C51E7C09A1009F8BF7 /* PasswordlessRouter.swift in Sources */,
5F92C68F1D50EAC200CCE6C0 /* LazyImage.swift in Sources */,
5F2496BA1D665AE900A1C6E2 /* CredentialAuthError.swift in Sources */,
- 5B3DA2531E6DE88900370C17 /* InternationalPhoneInputView.swift in Sources */,
5B9A54461E4B2CB4004B5454 /* PasswordlessAuthenticatable.swift in Sources */,
5B55F3CB1E242A2E00B75CF5 /* UnrecoverableErrorPresenter.swift in Sources */,
5F57DFC61D4F79DD00C54DA8 /* AuthStyle.swift in Sources */,
5B0971821DC8FAC5003AA88F /* EnterpriseDomainView.swift in Sources */,
5B4DE0191DD670F7004C8AC2 /* EnterpriseActiveAuthView.swift in Sources */,
+ 5BBD9E311E8E6F5E007EF625 /* OnePasswordExtension.m in Sources */,
5B9A544A1E4B354A004B5454 /* PasswordlessPresenter.swift in Sources */,
5FBE5CC61D3E7F9D0038536D /* MultifactorPresenter.swift in Sources */,
5FEAE2101D1A5691005C0028 /* HeaderView.swift in Sources */,
@@ -1189,6 +1223,7 @@
5FBE5CBA1D3E59B90038536D /* MultifactorCodeView.swift in Sources */,
5F1C49901D8360BF005B74FC /* ConnectionLoadingPresenter.swift in Sources */,
5F51EE6A1D1CBC830024BCD6 /* SingleInputView.swift in Sources */,
+ 5B3874CA1E97C0C600244326 /* InternationalPhoneInputView.swift in Sources */,
5B3440C31E7C0987009F8BF7 /* ClassicRouter.swift in Sources */,
5F99AA881D1B0E2500D27842 /* Layout.swift in Sources */,
5F2496BC1D665AF700A1C6E2 /* DatabaseUserCreatorError.swift in Sources */,
@@ -1221,13 +1256,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5BCB26861E8437BC00CFBF9A /* PolicyViewSpec.swift in Sources */,
5FE50DBF1D7A254000D82290 /* ConnectionLoadingPresenterSpec.swift in Sources */,
5F6C01551D91656100198ACD /* UsernameValidatorSpec.swift in Sources */,
5F2496BE1D67ADB300A1C6E2 /* EmailValidatorSpec.swift in Sources */,
5B0CF2D61DE9AE0F00F82BF4 /* InputValidationErrorSpec.swift in Sources */,
5B3440CC1E7C11EB009F8BF7 /* PasswordlessRouterSpec.swift in Sources */,
5FBE5CCA1D3EA1380038536D /* MultifactorPresenterSpec.swift in Sources */,
+ 5BB142EE1E97C2F000315607 /* CountryTableViewControllerSpec.swift in Sources */,
5BA563F11DD117550002D3AB /* EnterpriseActiveAuthInteractorSpec.swift in Sources */,
5BB78CC51E54BD530074DFE3 /* PasswordlessPresenterSpec.swift in Sources */,
5F92C68D1D50E47100CCE6C0 /* AuthStyleSpec.swift in Sources */,
@@ -1235,12 +1270,13 @@
5F0FCF921E201CF300E3D53B /* ObserverStoreSpec.swift in Sources */,
5B55F3D41E24FFD000B75CF5 /* UnrecoverableErrorSpec.swift in Sources */,
5F73CDDC1D309BE900D8D8D1 /* DatabasePasswordInteractorSpec.swift in Sources */,
- 5F2496AF1D66210500A1C6E2 /* LockViewControllerSpec.swift in Sources */,
+ 5B38F84F1E93F2A300C25EAC /* OnePasswordSpec.swift in Sources */,
5FA250501D48E2A200C544FA /* OptionsSpec.swift in Sources */,
5FBE5CC21D3E5EF50038536D /* MultifactorInteractorSpec.swift in Sources */,
+ 5B3874D51E97C14000244326 /* PolicyViewSpec.swift in Sources */,
5B1FD9721E546F500055C1AC /* PasswordlessInteractorSpec.swift in Sources */,
5F73CDDE1D30B16900D8D8D1 /* DatabaseForgotPasswordPresenterSpec.swift in Sources */,
- 5BE978201E82E7FA0090EC07 /* CountryTableViewControllerSpec.swift in Sources */,
+ 5BB142EF1E97C2F300315607 /* LockViewControllerSpec.swift in Sources */,
5F9231D51D5B6C5E00D92580 /* AuthCollectionViewSpec.swift in Sources */,
5F508FFB1D1DB1E700EAA650 /* DatabaseInteractorSpec.swift in Sources */,
5F5F98D41D21E3890016FC22 /* DatabasePresenterSpec.swift in Sources */,
@@ -1250,6 +1286,7 @@
5FBE5CCD1D3EDF960038536D /* UserSpec.swift in Sources */,
5BCDE1361DDDF17F00AA2A6C /* EnterpriseActiveAuthPresenterSpec.swift in Sources */,
5FBE5CC81D3EA0EA0038536D /* Mocks.swift in Sources */,
+ 5B3874D41E97C13D00244326 /* CountryCodesSpec.swift in Sources */,
5F57DFCE1D4FBE5A00C54DA8 /* AuthPresenterSpec.swift in Sources */,
5F92C6911D510AFE00CCE6C0 /* LazyImageSpec.swift in Sources */,
5BCED4C71DD1FEAA00E2CE8A /* EnterpriseDomainPresenterSpec.swift in Sources */,
@@ -1261,7 +1298,6 @@
5FE50DBD1D79B8AD00D82290 /* CDNLoaderInteractorSpec.swift in Sources */,
5F390E8D1D63B99300FC549C /* LoggerSpec.swift in Sources */,
5B0CF2DE1DE9B4AF00F82BF4 /* DatabaseUserCreatorErrorSpec.swift in Sources */,
- 5BBEED381E79A9F900B3308D /* CountryCodesSpec.swift in Sources */,
5F2037C21D5D02880005D2E2 /* Matchers.swift in Sources */,
5BB78CC21E54BC730074DFE3 /* PasswordlessAuthenticatableErrorSpec.swift in Sources */,
5FEEE81E1DB84BBB00B4DFED /* PasswordPolicySpec.swift in Sources */,
@@ -1276,10 +1312,10 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 5B124ED31EA8D15E0050E567 /* PBXTargetDependency */ = {
+ 5BEDE1401EC0A9750007300D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5FEADCF31D1A7EBC0032D810 /* LockApp */;
- targetProxy = 5B124ED21EA8D15E0050E567 /* PBXContainerItemProxy */;
+ targetProxy = 5BEDE13F1EC0A9750007300D /* PBXContainerItemProxy */;
};
5FEADD091D1A7F010032D810 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
@@ -1323,38 +1359,34 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
- 5B124ED41EA8D15E0050E567 /* Debug */ = {
+ 5BEDE1421EC0A9750007300D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = LockSnapshot/Info.plist;
+ INFOPLIST_FILE = LockUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.auth0.LockSnapshot;
+ PRODUCT_BUNDLE_IDENTIFIER = com.auth0.LockUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE = "";
- PROVISIONING_PROFILE_SPECIFIER = "";
+ PROVISIONING_PROFILE = "97f7985c-a5ce-42be-a3e5-39a6c818bc78";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 3.0;
TEST_TARGET_NAME = LockApp;
};
name = Debug;
};
- 5B124ED51EA8D15E0050E567 /* Release */ = {
+ 5BEDE1431EC0A9750007300D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = LockSnapshot/Info.plist;
+ INFOPLIST_FILE = LockUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
- PRODUCT_BUNDLE_IDENTIFIER = com.auth0.LockSnapshot;
+ PRODUCT_BUNDLE_IDENTIFIER = com.auth0.LockUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE = "";
- PROVISIONING_PROFILE_SPECIFIER = "";
+ PROVISIONING_PROFILE = "97f7985c-a5ce-42be-a3e5-39a6c818bc78";
SWIFT_VERSION = 3.0;
TEST_TARGET_NAME = LockApp;
};
@@ -1523,6 +1555,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.auth0.ios.Lock;
PRODUCT_NAME = Lock;
SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
};
@@ -1548,6 +1581,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.auth0.ios.Lock;
PRODUCT_NAME = Lock;
SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 3.0;
};
name = Release;
@@ -1589,11 +1623,11 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- 5B124ED61EA8D15E0050E567 /* Build configuration list for PBXNativeTarget "LockSnapshot" */ = {
+ 5BEDE1411EC0A9750007300D /* Build configuration list for PBXNativeTarget "LockUITests.iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 5B124ED41EA8D15E0050E567 /* Debug */,
- 5B124ED51EA8D15E0050E567 /* Release */,
+ 5BEDE1421EC0A9750007300D /* Debug */,
+ 5BEDE1431EC0A9750007300D /* Release */,
);
defaultConfigurationIsVisible = 0;
};
diff --git a/Lock/ClassicRouter.swift b/Lock/ClassicRouter.swift
index 5599922e4..4e911e6c9 100644
--- a/Lock/ClassicRouter.swift
+++ b/Lock/ClassicRouter.swift
@@ -51,6 +51,7 @@ struct ClassicRouter: Router {
guard self.lock.options.allow != [.ResetPassword] && self.lock.options.initialScreen != .resetPassword else { return forgotPassword }
let authentication = self.lock.authentication
let interactor = DatabaseInteractor(connection: database, authentication: authentication, user: self.user, options: self.lock.options, dispatcher: lock.observerStore)
+ self.lock.options.passwordManager.controller = self.controller
let presenter = DatabasePresenter(interactor: interactor, connection: database, navigator: self, options: self.lock.options)
if !oauth2.isEmpty {
let interactor = Auth0OAuth2Interactor(authentication: self.lock.authentication, dispatcher: lock.observerStore, options: self.lock.options, nativeHandlers: self.lock.nativeHandlers)
diff --git a/Lock/DatabaseOnlyView.swift b/Lock/DatabaseOnlyView.swift
index 16e3b3d47..42321f936 100644
--- a/Lock/DatabaseOnlyView.swift
+++ b/Lock/DatabaseOnlyView.swift
@@ -34,6 +34,10 @@ class DatabaseOnlyView: UIView, DatabaseView {
weak var ssoBar: InfoBarView?
weak var spacer: UIView?
private var style: Style?
+ weak var passwordManagerButton: IconButton?
+
+ weak var identityField: InputField?
+ weak var passwordField: InputField?
// FIXME: Remove this from the view since it should not even know it exists
var navigator: Navigable?
@@ -86,7 +90,7 @@ class DatabaseOnlyView: UIView, DatabaseView {
private let separatorIndex = 2
private let socialIndex = 1
- func showLogin(withIdentifierStyle style: DatabaseIdentifierStyle, identifier: String? = nil, authCollectionView: AuthCollectionView? = nil) {
+ func showLogin(withIdentifierStyle style: DatabaseIdentifierStyle, identifier: String? = nil, authCollectionView: AuthCollectionView? = nil, showPassswordManager: Bool) {
let form = CredentialView()
let type: InputField.InputType
@@ -108,9 +112,16 @@ class DatabaseOnlyView: UIView, DatabaseView {
layoutInStack(form, authCollectionView: authCollectionView)
self.layoutSecondaryButton(self.allowedModes.contains(.ResetPassword))
self.form = form
+ self.identityField = form.identityField
+ self.passwordField = form.passwordField
+
+ if showPassswordManager {
+ self.passwordManagerButton = form.passwordField.addFieldButton(withIcon: "ic_onepassword", color: Style.Auth0.onePasswordIconColor)
+ }
+
}
- func showSignUp(withUsername showUsername: Bool, username: String?, email: String?, authCollectionView: AuthCollectionView? = nil, additionalFields: [CustomTextField], passwordPolicyValidator: PasswordPolicyValidator? = nil) {
+ func showSignUp(withUsername showUsername: Bool, username: String?, email: String?, authCollectionView: AuthCollectionView? = nil, additionalFields: [CustomTextField], passwordPolicyValidator: PasswordPolicyValidator? = nil, showPassswordManager: Bool) {
let form = SignUpView(additionalFields: additionalFields)
form.showUsername = showUsername
form.emailField.text = email
@@ -125,6 +136,9 @@ class DatabaseOnlyView: UIView, DatabaseView {
self.layoutSecondaryButton(true)
self.form = form
+ self.identityField = showUsername ? form.usernameField : form.emailField
+ self.passwordField = form.passwordField
+
if let passwordPolicyValidator = passwordPolicyValidator {
let passwordPolicyView = PolicyView(rules: passwordPolicyValidator.policy.rules)
passwordPolicyValidator.delegate = passwordPolicyView
@@ -146,6 +160,10 @@ class DatabaseOnlyView: UIView, DatabaseView {
view.isHidden = true
}
}
+
+ if showPassswordManager {
+ self.passwordManagerButton = form.passwordField.addFieldButton(withIcon: "ic_onepassword", color: Style.Auth0.onePasswordIconColor)
+ }
}
func presentEnterprise() {
@@ -244,5 +262,6 @@ class DatabaseOnlyView: UIView, DatabaseView {
func apply(style: Style) {
self.style = style
self.separator?.textColor = style.seperatorTextColor
+ self.passwordManagerButton?.color = style.onePasswordIconColor
}
}
diff --git a/Lock/DatabasePresenter.swift b/Lock/DatabasePresenter.swift
index f7ddd00f2..88339a146 100644
--- a/Lock/DatabasePresenter.swift
+++ b/Lock/DatabasePresenter.swift
@@ -38,6 +38,8 @@ class DatabasePresenter: Presentable, Loggable {
}
}
+ var passwordManager: PasswordManager
+
var authPresenter: AuthPresenter?
var enterpriseInteractor: EnterpriseDomainInteractor?
@@ -57,6 +59,7 @@ class DatabasePresenter: Presentable, Loggable {
self.database = connection
self.navigator = navigator
self.options = options
+ self.passwordManager = options.passwordManager
}
var view: View {
@@ -85,6 +88,7 @@ class DatabasePresenter: Presentable, Loggable {
showSignup(inView: database, username: initialUsername, email: initialEmail)
}
self.databaseView = database
+
return database
}
@@ -92,11 +96,10 @@ class DatabasePresenter: Presentable, Loggable {
self.messagePresenter?.hideCurrent()
let authCollectionView = self.authPresenter?.newViewToEmbed(withInsets: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20), isLogin: true)
let style = self.database.requiresUsername ? self.options.usernameStyle : [.Email]
- view.showLogin(withIdentifierStyle: style, identifier: identifier, authCollectionView: authCollectionView)
+ view.showLogin(withIdentifierStyle: style, identifier: identifier, authCollectionView: authCollectionView, showPassswordManager: self.passwordManager.available)
self.currentScreen = .login
let form = view.form
form?.onValueChange = self.handleInput
-
let action = { [weak form] (button: PrimaryButton) in
self.messagePresenter?.hideCurrent()
self.logger.info("Perform login for email: \(self.authenticator.email.verbatim())")
@@ -146,6 +149,23 @@ class DatabasePresenter: Presentable, Loggable {
view.secondaryButton?.onPress = { button in
self.navigator.navigate(.forgotPassword)
}
+
+ if let identifyField = view.identityField, let passwordField = view.passwordField {
+ passwordManager.onUpdate = { [unowned self, unowned identifyField, unowned passwordField] identifier, password in
+ identifyField.text = identifier
+ passwordField.text = password
+ self.handleInput(identifyField)
+ self.handleInput(passwordField)
+ }
+ }
+ view.passwordManagerButton?.onPress = { _ in
+ self.passwordManager.login {
+ guard $0 == nil else {
+ return self.logger.error("There was a problem with the password manager: \($0.verbatim())")
+ }
+ }
+ }
+
}
private func showSignup(inView view: DatabaseView, username: String?, email: String?) {
@@ -155,7 +175,7 @@ class DatabasePresenter: Presentable, Loggable {
let passwordPolicyValidator = interactor?.passwordValidator as? PasswordPolicyValidator
self.currentScreen = .signup
- view.showSignUp(withUsername: self.database.requiresUsername, username: username, email: email, authCollectionView: authCollectionView, additionalFields: self.options.customSignupFields, passwordPolicyValidator: passwordPolicyValidator)
+ view.showSignUp(withUsername: self.database.requiresUsername, username: username, email: email, authCollectionView: authCollectionView, additionalFields: self.options.customSignupFields, passwordPolicyValidator: passwordPolicyValidator, showPassswordManager: self.passwordManager.available)
let form = view.form
view.form?.onValueChange = self.handleInput
let action = { [weak form] (button: PrimaryButton) in
@@ -186,7 +206,6 @@ class DatabasePresenter: Presentable, Loggable {
form?.needsToUpdateState()
self.messagePresenter?.showError(error)
self.logger.error("Failed with error \(error)")
-
}
}
}
@@ -206,6 +225,23 @@ class DatabasePresenter: Presentable, Loggable {
[cancel, tos, privacy].forEach { alert.addAction($0) }
self.navigator.present(alert)
}
+
+ if let identifyField = view.identityField, let passwordField = view.passwordField {
+ passwordManager.onUpdate = { [unowned self, unowned identifyField, unowned passwordField] identifier, password in
+ identifyField.text = identifier
+ passwordField.text = password
+ self.handleInput(identifyField)
+ self.handleInput(passwordField)
+ }
+ }
+ view.passwordManagerButton?.onPress = { _ in
+ self.passwordManager.store(withPolicy: passwordPolicyValidator?.policy.onePasswordRules(), identifier: self.creator.identifier) {
+ guard $0 == nil else {
+ return self.logger.error("There was a problem with the password manager: \($0.verbatim())")
+ }
+ }
+ }
+
}
private func handleInput(_ input: InputField) {
@@ -238,9 +274,9 @@ class DatabasePresenter: Presentable, Loggable {
input.showValid()
guard
- let mode = self.databaseView?.switcher?.selected,
- mode == .login && updateHRD
- else { return }
+ let mode = self.databaseView?.switcher?.selected,
+ mode == .login && updateHRD
+ else { return }
try? self.enterpriseInteractor?.updateEmail(input.text)
if let connection = self.enterpriseInteractor?.connection {
self.logger.verbose("Enterprise connection detected: \(connection)")
diff --git a/Lock/DatabaseView.swift b/Lock/DatabaseView.swift
index fe70526f5..9b7ceb91b 100644
--- a/Lock/DatabaseView.swift
+++ b/Lock/DatabaseView.swift
@@ -27,12 +27,15 @@ protocol DatabaseView: class, View {
weak var secondaryButton: SecondaryButton? { get }
weak var primaryButton: PrimaryButton? { get }
weak var switcher: DatabaseModeSwitcher? { get }
+ weak var passwordManagerButton: IconButton? { get }
+ weak var identityField: InputField? { get }
+ weak var passwordField: InputField? { get }
var traitCollection: UITraitCollection { get }
var allowedModes: DatabaseMode { get }
- func showLogin(withIdentifierStyle style: DatabaseIdentifierStyle, identifier: String?, authCollectionView: AuthCollectionView?)
+ func showLogin(withIdentifierStyle style: DatabaseIdentifierStyle, identifier: String?, authCollectionView: AuthCollectionView?, showPassswordManager: Bool)
// swiftlint:disable:next function_parameter_count
- func showSignUp(withUsername showUsername: Bool, username: String?, email: String?, authCollectionView: AuthCollectionView?, additionalFields: [CustomTextField], passwordPolicyValidator: PasswordPolicyValidator?)
+ func showSignUp(withUsername showUsername: Bool, username: String?, email: String?, authCollectionView: AuthCollectionView?, additionalFields: [CustomTextField], passwordPolicyValidator: PasswordPolicyValidator?, showPassswordManager: Bool)
}
diff --git a/Lock/IconButton.swift b/Lock/IconButton.swift
new file mode 100644
index 000000000..2a4b43b5f
--- /dev/null
+++ b/Lock/IconButton.swift
@@ -0,0 +1,82 @@
+// IconButton.swift.swift
+//
+// Copyright (c) 2017 Auth0 (http://auth0.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import UIKit
+
+class IconButton: UIView {
+
+ weak var button: UIButton?
+
+ var onPress: (IconButton) -> Void = {_ in }
+
+ var color: UIColor = .clear {
+ didSet {
+ self.button?.tintColor = self.color
+ }
+ }
+
+ var icon: UIImage? {
+ didSet {
+ self.button?.setImage(self.icon, for: .normal)
+ }
+ }
+
+ // MARK: - Initialisers
+ convenience init() {
+ self.init(frame: CGRect.zero)
+ }
+
+ required override init(frame: CGRect) {
+ super.init(frame: frame)
+ self.layoutButton()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ self.layoutButton()
+ }
+
+ // MARK: - Layout
+
+ private func layoutButton() {
+ let button = UIButton(type: .custom)
+ self.addSubview(button)
+
+ constraintEqual(anchor: button.topAnchor, toAnchor: self.topAnchor)
+ constraintEqual(anchor: button.leftAnchor, toAnchor: self.leftAnchor)
+ constraintEqual(anchor: button.rightAnchor, toAnchor: self.rightAnchor)
+ constraintEqual(anchor: button.bottomAnchor, toAnchor: self.bottomAnchor)
+ button.translatesAutoresizingMaskIntoConstraints = false
+
+ button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
+ button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
+ self.button = button
+ }
+
+ func pressed(_ sender: Any) {
+ self.onPress(self)
+ }
+
+ override var intrinsicContentSize: CGSize {
+ return CGSize(width: 50, height: UIViewNoIntrinsicMetric)
+ }
+}
diff --git a/Lock/InputField.swift b/Lock/InputField.swift
index a2055d79d..d4bcb97a8 100644
--- a/Lock/InputField.swift
+++ b/Lock/InputField.swift
@@ -33,6 +33,7 @@ class InputField: UIView, UITextFieldDelegate, Stylable {
private weak var errorLabelTopPadding: NSLayoutConstraint?
private weak var textFieldLeftAnchor: NSLayoutConstraint?
+ private weak var textFieldRightPadding: NSLayoutConstraint?
private(set) var state: State = .invalid(nil)
private weak var borderColor: UIColor?
private weak var borderColorError: UIColor?
@@ -163,7 +164,7 @@ class InputField: UIView, UITextFieldDelegate, Stylable {
self.textFieldLeftAnchor = constraintEqual(anchor: textField.leftAnchor, toAnchor: iconContainer.rightAnchor, constant: 16)
constraintEqual(anchor: textField.topAnchor, toAnchor: container.topAnchor)
- constraintEqual(anchor: textField.rightAnchor, toAnchor: container.rightAnchor, constant: -16)
+ self.textFieldRightPadding = constraintEqual(anchor: textField.rightAnchor, toAnchor: container.rightAnchor, constant: -16)
constraintEqual(anchor: textField.bottomAnchor, toAnchor: container.bottomAnchor)
dimension(dimension: textField.heightAnchor, withValue: 50)
textField.translatesAutoresizingMaskIntoConstraints = false
@@ -199,6 +200,27 @@ class InputField: UIView, UITextFieldDelegate, Stylable {
return CGSize(width: 230, height: 50)
}
+ // MARK: - Password Manager
+
+ func addFieldButton(withIcon name: String, color: UIColor = .black) -> IconButton? {
+ guard let container = self.containerView, let textField = self.textField else { return nil }
+
+ let button = IconButton()
+ button.icon = LazyImage(name: name, bundle: Lock.bundle).image(compatibleWithTraits: self.traitCollection)
+ button.color = color
+ container.addSubview(button)
+
+ self.textFieldRightPadding?.isActive = false
+ constraintEqual(anchor: textField.rightAnchor, toAnchor: button.leftAnchor)
+ constraintEqual(anchor: button.leftAnchor, toAnchor: textField.rightAnchor)
+ constraintEqual(anchor: button.topAnchor, toAnchor: textField.topAnchor)
+ constraintEqual(anchor: button.bottomAnchor, toAnchor: textField.bottomAnchor)
+ constraintEqual(anchor: button.rightAnchor, toAnchor: container.rightAnchor)
+ button.translatesAutoresizingMaskIntoConstraints = false
+
+ return button
+ }
+
// MARK: - Internal
enum State {
diff --git a/Lock/Lock.h b/Lock/Lock.h
index 2f09a5e16..91df86475 100644
--- a/Lock/Lock.h
+++ b/Lock/Lock.h
@@ -30,4 +30,6 @@ FOUNDATION_EXPORT const unsigned char LockVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import
+#import "OnePasswordExtension.h"
+
diff --git a/Lock/Lock.xcassets/ic_onepassword.imageset/Contents.json b/Lock/Lock.xcassets/ic_onepassword.imageset/Contents.json
new file mode 100644
index 000000000..f7016e54e
--- /dev/null
+++ b/Lock/Lock.xcassets/ic_onepassword.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_onepassword.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
diff --git a/Lock/Lock.xcassets/ic_onepassword.imageset/ic_onepassword.pdf b/Lock/Lock.xcassets/ic_onepassword.imageset/ic_onepassword.pdf
new file mode 100644
index 000000000..83eb836dd
Binary files /dev/null and b/Lock/Lock.xcassets/ic_onepassword.imageset/ic_onepassword.pdf differ
diff --git a/Lock/LockOptions.swift b/Lock/LockOptions.swift
index 9c3293b13..3dcd8ce3e 100644
--- a/Lock/LockOptions.swift
+++ b/Lock/LockOptions.swift
@@ -48,4 +48,5 @@ struct LockOptions: OptionBuildable {
var audience: String?
var passwordlessMethod: PasswordlessMethod = .code
+ var passwordManager: OnePassword = OnePassword()
}
diff --git a/Lock/OnePassword.swift b/Lock/OnePassword.swift
new file mode 100644
index 000000000..e149c17a9
--- /dev/null
+++ b/Lock/OnePassword.swift
@@ -0,0 +1,88 @@
+// OnePassword.swift
+//
+// Copyright (c) 2017 Auth0 (http://auth0.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+
+protocol PasswordManager {
+
+ var enabled: Bool { get set }
+ var available: Bool { get }
+
+ var onUpdate: (String, String) -> Void { get set }
+
+ func login(callback: @escaping (Error?) -> Void)
+ func store(withPolicy policy: [String: Any]?, identifier: String?, callback: @escaping (Error?) -> Void)
+}
+
+public class OnePassword: PasswordManager {
+
+ /// A Boolean value indicating whether the password manager is enabled.
+ public var enabled: Bool = true
+
+ /// The text identifier to use with the password manager to identify which credentials to use.
+ public var appIdentifier: String
+
+ /// The title to be displayed when creating a new password manager entry.
+ public var displayName: String
+
+ weak var controller: UIViewController?
+
+ public init() {
+ self.appIdentifier = Bundle.main.bundleIdentifier.verbatim()
+ self.displayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName").verbatim()
+ }
+
+ var available: Bool {
+ return self.enabled && OnePasswordExtension.shared().isAppExtensionAvailable()
+ }
+
+ var onUpdate: (String, String) -> Void = { _ in }
+
+ func login(callback: @escaping (Error?) -> Void) {
+ guard let controller = self.controller else { return }
+ OnePasswordExtension.shared().findLogin(forURLString: self.appIdentifier, for: controller, sender: nil) { (result, error) in
+ guard error == nil else {
+ return callback(error)
+ }
+ self.handleResut(result)
+ callback(nil)
+ }
+ }
+
+ func store(withPolicy policy: [String: Any]?, identifier: String?, callback: @escaping (Error?) -> Void) {
+ guard let controller = self.controller else { return }
+ var loginDetails: [String: String] = [ AppExtensionTitleKey: self.displayName ]
+ loginDetails[AppExtensionUsernameKey] = identifier
+ OnePasswordExtension.shared().storeLogin(forURLString: self.appIdentifier, loginDetails: loginDetails, passwordGenerationOptions: policy, for: controller, sender: nil) { (result, error) in
+ guard error == nil else {
+ return callback(error)
+ }
+ self.handleResut(result)
+ callback(nil)
+ }
+ }
+
+ private func handleResut(_ dict: [AnyHashable : Any]?) {
+ guard let username = dict?[AppExtensionUsernameKey] as? String, let password = dict?[AppExtensionPasswordKey] as? String else { return }
+ self.onUpdate(username, password)
+ }
+}
diff --git a/Lock/OnePasswordExtension.h b/Lock/OnePasswordExtension.h
new file mode 100644
index 000000000..25d63dc83
--- /dev/null
+++ b/Lock/OnePasswordExtension.h
@@ -0,0 +1,210 @@
+//
+// 1Password Extension
+//
+// Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov.
+// Copyright (c) 2014 AgileBits. All rights reserved.
+//
+
+#import
+#import
+#import
+
+#ifdef __IPHONE_8_0
+#import
+#endif
+
+#if __has_feature(nullability)
+NS_ASSUME_NONNULL_BEGIN
+#else
+#define nullable
+#define __nullable
+#define nonnull
+#define __nonnull
+#endif
+
+// Login Dictionary keys - Used to get or set the properties of a 1Password Login
+#define AppExtensionURLStringKey @"url_string"
+#define AppExtensionUsernameKey @"username"
+#define AppExtensionPasswordKey @"password"
+#define AppExtensionTOTPKey @"totp"
+#define AppExtensionTitleKey @"login_title"
+#define AppExtensionNotesKey @"notes"
+#define AppExtensionSectionTitleKey @"section_title"
+#define AppExtensionFieldsKey @"fields"
+#define AppExtensionReturnedFieldsKey @"returned_fields"
+#define AppExtensionOldPasswordKey @"old_password"
+#define AppExtensionPasswordGeneratorOptionsKey @"password_generator_options"
+
+// Password Generator options - Used to set the 1Password Password Generator options when saving a new Login or when changing the password for for an existing Login
+#define AppExtensionGeneratedPasswordMinLengthKey @"password_min_length"
+#define AppExtensionGeneratedPasswordMaxLengthKey @"password_max_length"
+#define AppExtensionGeneratedPasswordRequireDigitsKey @"password_require_digits"
+#define AppExtensionGeneratedPasswordRequireSymbolsKey @"password_require_symbols"
+#define AppExtensionGeneratedPasswordForbiddenCharactersKey @"password_forbidden_characters"
+
+// Errors codes
+#define AppExtensionErrorDomain @"OnePasswordExtension"
+
+#define AppExtensionErrorCodeCancelledByUser 0
+#define AppExtensionErrorCodeAPINotAvailable 1
+#define AppExtensionErrorCodeFailedToContactExtension 2
+#define AppExtensionErrorCodeFailedToLoadItemProviderData 3
+#define AppExtensionErrorCodeCollectFieldsScriptFailed 4
+#define AppExtensionErrorCodeFillFieldsScriptFailed 5
+#define AppExtensionErrorCodeUnexpectedData 6
+#define AppExtensionErrorCodeFailedToObtainURLStringFromWebView 7
+
+// Note to creators of libraries or frameworks:
+// If you include this code within your library, then to prevent potential duplicate symbol
+// conflicts for adopters of your library, you should rename the OnePasswordExtension class
+// and associated typedefs. You might to so by adding your own project prefix, e.g.,
+// MyLibraryOnePasswordExtension.
+
+typedef void (^OnePasswordLoginDictionaryCompletionBlock)(NSDictionary * __nullable loginDictionary, NSError * __nullable error);
+typedef void (^OnePasswordSuccessCompletionBlock)(BOOL success, NSError * __nullable error);
+typedef void (^OnePasswordExtensionItemCompletionBlock)(NSExtensionItem * __nullable extensionItem, NSError * __nullable error);
+
+@interface OnePasswordExtension : NSObject
+
++ (OnePasswordExtension *)sharedExtension;
+
+/*!
+ @discussion Determines if the 1Password Extension is available. Allows you to only show the 1Password login button to those
+ that can use it. Of course, you could leave the button enabled and educate users about the virtues of strong, unique
+ passwords instead :)
+
+ @return isAppExtensionAvailable Returns YES if any app that supports the generic `org-appextension-feature-password-management` feature is installed on the device.
+ */
+#ifdef __IPHONE_8_0
+- (BOOL)isAppExtensionAvailable NS_EXTENSION_UNAVAILABLE_IOS("Not available in an extension. Check if org-appextension-feature-password-management:// URL can be opened by the app.");
+#else
+- (BOOL)isAppExtensionAvailable;
+#endif
+
+/*!
+ Called from your login page, this method will find all available logins for the given URLString.
+
+ @discussion 1Password will show all matching Login for the naked domain of the given URLString. For example if the user has an item in your 1Password vault with "subdomain1.domain.com” as the website and another one with "subdomain2.domain.com”, and the URLString is "https://domain.com", 1Password will show both items.
+
+ However, if no matching login is found for "https://domain.com", the 1Password Extension will display the "Show all Logins" button so that the user can search among all the Logins in the vault. This is especially useful when the user has a login for "https://olddomain.com".
+
+ After the user selects a login, it is stored into an NSDictionary and given to your completion handler. Use the `Login Dictionary keys` above to
+ extract the needed information and update your UI. The completion block is guaranteed to be called on the main thread.
+
+ @param URLString For the matching Logins in the 1Password vault.
+
+ @param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
+
+ @param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
+
+ @param completion A completion block called with two parameters loginDictionary and error once completed. The loginDictionary reply parameter that contains the username, password and the One-Time Password if available. The error Reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
+ */
+- (void)findLoginForURLString:(nonnull NSString *)URLString forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
+
+/*!
+ Create a new login within 1Password and allow the user to generate a new password before saving.
+
+ @discussion The provided URLString should be unique to your app or service and be identical to what you pass into the find login method.
+ The completion block is guaranteed to be called on the main
+ thread.
+
+ @param URLString For the new Login to be saved in 1Password.
+
+ @param loginDetailsDictionary about the Login to be saved, including custom fields, are stored in an dictionary and given to the 1Password Extension.
+
+ @param passwordGenerationOptions The Password generator options represented in a dictionary form.
+
+ @param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
+
+ @param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
+
+ @param completion A completion block which is called with type parameters loginDictionary and error. The loginDictionary reply parameter which contain all the information about the newly saved Login. Use the `Login Dictionary keys` above to extract the needed information and update your UI. For example, updating the UI with the newly generated password lets the user know their action was successful. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
+ */
+- (void)storeLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
+
+/*!
+ Change the password for an existing login within 1Password.
+
+ @discussion The provided URLString should be unique to your app or service and be identical to what you pass into the find login method. The completion block is guaranteed to be called on the main thread.
+
+ 1Password 6 and later:
+ The 1Password Extension will display all available the matching Logins for the given URL string. The user can choose which Login item to update. The "New Login" button will also be available at all times, in case the user wishes to to create a new Login instead,
+
+ 1Password 5:
+ These are the three scenarios that are supported:
+ 1. A single matching Login is found: 1Password will enter edit mode for that Login and will update its password using the value for AppExtensionPasswordKey.
+ 2. More than a one matching Logins are found: 1Password will display a list of all matching Logins. The user must choose which one to update. Once in edit mode, the Login will be updated with the new password.
+ 3. No matching login is found: 1Password will create a new Login using the optional fields if available to populate its properties.
+
+ @param URLString for the Login to be updated with a new password in 1Password.
+
+ @param loginDetailsDictionary about the Login to be saved, including old password and the username, are stored in an dictionary and given to the 1Password Extension.
+
+ @param passwordGenerationOptions The Password generator options epresented in a dictionary form.
+
+ @param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
+
+ @param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
+
+ @param completion A completion block which is called with type parameters loginDictionary and error. The loginDictionary reply parameter which contain all the information about the newly updated Login, including the newly generated and the old password. Use the `Login Dictionary keys` above to extract the needed information and update your UI. For example, updating the UI with the newly generated password lets the user know their action was successful. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
+ */
+- (void)changePasswordForLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion;
+
+/*!
+ Called from your web view controller, this method will show all the saved logins for the active page in the provided web
+ view, and automatically fill the HTML form fields. Supports both WKWebView and UIWebView.
+
+ @discussion 1Password will show all matching Login for the naked domain of the current website. For example if the user has an item in your 1Password vault with "subdomain1.domain.com” as the website and another one with "subdomain2.domain.com”, and the current website is "https://domain.com", 1Password will show both items.
+
+ However, if no matching login is found for "https://domain.com", the 1Password Extension will display the "New Login" button so that the user can create a new Login for the current website.
+
+ @param webView The web view which displays the form to be filled. The active UIWebView Or WKWebView. Must not be nil.
+
+ @param viewController The view controller from which the 1Password Extension is invoked. Usually `self`
+
+ @param sender The sender which triggers the share sheet to show. UIButton, UIBarButtonItem or UIView. Can also be nil on iPhone, but not on iPad.
+
+ @param yesOrNo Boolean flag. If YES is passed only matching Login items will be shown, otherwise the 1Password Extension will also display Credit Cards and Identities.
+
+ @param completion Completion block called on completion with parameters success, and error. The success reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
+ */
+- (void)fillItemIntoWebView:(nonnull id)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion;
+
+/*!
+ Called in the UIActivityViewController completion block to find out whether or not the user selected the 1Password Extension activity.
+
+ @param activityType or the bundle identidier of the selected activity in the share sheet.
+
+ @return isOnePasswordExtensionActivityType Returns YES if the selected activity is the 1Password extension, NO otherwise.
+ */
+- (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType;
+
+/*!
+ The returned NSExtensionItem can be used to create your own UIActivityViewController. Use `isOnePasswordExtensionActivityType:` and `fillReturnedItems:intoWebView:completion:` in the activity view controller completion block to process the result. The completion block is guaranteed to be called on the main thread.
+
+ @param webView The web view which displays the form to be filled. The active UIWebView Or WKWebView. Must not be nil.
+
+ @param completion Completion block called on completion with extensionItem and error. The extensionItem reply parameter that is contains all the info required by the 1Password extension if has been successfully completed or nil otherwise. The error reply parameter that is nil if the 1Password extension item has been successfully created, or it contains error information about the completion failure.
+ */
+- (void)createExtensionItemForWebView:(nonnull id)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion;
+
+/*!
+ Method used in the UIActivityViewController completion block to fill information into a web view.
+
+ @param returnedItems Array which contains the selected activity in the share sheet. Empty array if the share sheet is cancelled by the user.
+ @param webView The web view which displays the form to be filled. The active UIWebView Or WKWebView. Must not be nil.
+
+ @param completion Completion block called on completion with parameters success, and error. The success reply parameter that is YES if the 1Password Extension has been successfully completed or NO otherwise. The error reply parameter that is nil if the 1Password Extension has been successfully completed, or it contains error information about the completion failure.
+ */
+- (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull id)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion;
+
+/*!
+ Deprecated in version 1.5
+ @see Use fillItemIntoWebView:forViewController:sender:showOnlyLogins:completion: instead
+ */
+- (void)fillLoginIntoWebView:(nonnull id)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordSuccessCompletionBlock)completion __attribute__((deprecated("Use fillItemIntoWebView:forViewController:sender:showOnlyLogins:completion: instead. Deprecated in version 1.5")));
+@end
+
+#if __has_feature(nullability)
+NS_ASSUME_NONNULL_END
+#endif
diff --git a/Lock/OnePasswordExtension.m b/Lock/OnePasswordExtension.m
new file mode 100644
index 000000000..db0d10280
--- /dev/null
+++ b/Lock/OnePasswordExtension.m
@@ -0,0 +1,696 @@
+//
+// 1Password Extension
+//
+// Lovingly handcrafted by Dave Teare, Michael Fey, Rad Azzouz, and Roustem Karimov.
+// Copyright (c) 2014 AgileBits. All rights reserved.
+//
+
+#import "OnePasswordExtension.h"
+
+// Version
+#define VERSION_NUMBER @(184)
+static NSString *const AppExtensionVersionNumberKey = @"version_number";
+
+// Available App Extension Actions
+static NSString *const kUTTypeAppExtensionFindLoginAction = @"org.appextension.find-login-action";
+static NSString *const kUTTypeAppExtensionSaveLoginAction = @"org.appextension.save-login-action";
+static NSString *const kUTTypeAppExtensionChangePasswordAction = @"org.appextension.change-password-action";
+static NSString *const kUTTypeAppExtensionFillWebViewAction = @"org.appextension.fill-webview-action";
+static NSString *const kUTTypeAppExtensionFillBrowserAction = @"org.appextension.fill-browser-action";
+
+// WebView Dictionary keys
+static NSString *const AppExtensionWebViewPageFillScript = @"fillScript";
+static NSString *const AppExtensionWebViewPageDetails = @"pageDetails";
+
+@implementation OnePasswordExtension
+
+#pragma mark - Public Methods
+
++ (OnePasswordExtension *)sharedExtension {
+ static dispatch_once_t onceToken;
+ static OnePasswordExtension *__sharedExtension;
+
+ dispatch_once(&onceToken, ^{
+ __sharedExtension = [OnePasswordExtension new];
+ });
+
+ return __sharedExtension;
+}
+
+- (BOOL)isAppExtensionAvailable {
+ if ([self isSystemAppExtensionAPIAvailable]) {
+ return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"org-appextension-feature-password-management://"]];
+ }
+
+ return NO;
+}
+
+#pragma mark - Native app Login
+
+- (void)findLoginForURLString:(nonnull NSString *)URLString forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
+ NSAssert(URLString != nil, @"URLString must not be nil");
+ NSAssert(viewController != nil, @"viewController must not be nil");
+
+ if (NO == [self isSystemAppExtensionAPIAvailable]) {
+ NSLog(@"Failed to findLoginForURLString, system API is not available");
+ if (completion) {
+ completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
+ }
+
+ return;
+ }
+
+#ifdef __IPHONE_8_0
+ NSDictionary *item = @{ AppExtensionVersionNumberKey: VERSION_NUMBER, AppExtensionURLStringKey: URLString };
+
+ UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionFindLoginAction];
+ activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
+ if (returnedItems.count == 0) {
+ NSError *error = nil;
+ if (activityError) {
+ NSLog(@"Failed to findLoginForURLString: %@", activityError);
+ error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
+ }
+ else {
+ error = [OnePasswordExtension extensionCancelledByUserError];
+ }
+
+ if (completion) {
+ completion(nil, error);
+ }
+
+ return;
+ }
+
+ [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
+ if (completion) {
+ completion(itemDictionary, error);
+ }
+ }];
+ };
+
+ [viewController presentViewController:activityViewController animated:YES completion:nil];
+#endif
+}
+
+#pragma mark - New User Registration
+
+- (void)storeLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
+ NSAssert(URLString != nil, @"URLString must not be nil");
+ NSAssert(viewController != nil, @"viewController must not be nil");
+
+ if (NO == [self isSystemAppExtensionAPIAvailable]) {
+ NSLog(@"Failed to storeLoginForURLString, system API is not available");
+ if (completion) {
+ completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
+ }
+
+ return;
+ }
+
+
+#ifdef __IPHONE_8_0
+ NSMutableDictionary *newLoginAttributesDict = [NSMutableDictionary new];
+ newLoginAttributesDict[AppExtensionVersionNumberKey] = VERSION_NUMBER;
+ newLoginAttributesDict[AppExtensionURLStringKey] = URLString;
+ [newLoginAttributesDict addEntriesFromDictionary:loginDetailsDictionary];
+ if (passwordGenerationOptions.count > 0) {
+ newLoginAttributesDict[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions;
+ }
+
+ UIActivityViewController *activityViewController = [self activityViewControllerForItem:newLoginAttributesDict viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionSaveLoginAction];
+ activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
+ if (returnedItems.count == 0) {
+ NSError *error = nil;
+ if (activityError) {
+ NSLog(@"Failed to storeLoginForURLString: %@", activityError);
+ error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
+ }
+ else {
+ error = [OnePasswordExtension extensionCancelledByUserError];
+ }
+
+ if (completion) {
+ completion(nil, error);
+ }
+
+ return;
+ }
+
+ [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
+ if (completion) {
+ completion(itemDictionary, error);
+ }
+ }];
+ };
+
+ [viewController presentViewController:activityViewController animated:YES completion:nil];
+#endif
+}
+
+#pragma mark - Change Password
+
+- (void)changePasswordForLoginForURLString:(nonnull NSString *)URLString loginDetails:(nullable NSDictionary *)loginDetailsDictionary passwordGenerationOptions:(nullable NSDictionary *)passwordGenerationOptions forViewController:(UIViewController *)viewController sender:(nullable id)sender completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
+ NSAssert(URLString != nil, @"URLString must not be nil");
+ NSAssert(viewController != nil, @"viewController must not be nil");
+
+ if (NO == [self isSystemAppExtensionAPIAvailable]) {
+ NSLog(@"Failed to changePasswordForLoginWithUsername, system API is not available");
+ if (completion) {
+ completion(nil, [OnePasswordExtension systemAppExtensionAPINotAvailableError]);
+ }
+
+ return;
+ }
+
+#ifdef __IPHONE_8_0
+ NSMutableDictionary *item = [NSMutableDictionary new];
+ item[AppExtensionVersionNumberKey] = VERSION_NUMBER;
+ item[AppExtensionURLStringKey] = URLString;
+ [item addEntriesFromDictionary:loginDetailsDictionary];
+ if (passwordGenerationOptions.count > 0) {
+ item[AppExtensionPasswordGeneratorOptionsKey] = passwordGenerationOptions;
+ }
+
+ UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:viewController sender:sender typeIdentifier:kUTTypeAppExtensionChangePasswordAction];
+
+ activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
+ if (returnedItems.count == 0) {
+ NSError *error = nil;
+ if (activityError) {
+ NSLog(@"Failed to changePasswordForLoginWithUsername: %@", activityError);
+ error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
+ }
+ else {
+ error = [OnePasswordExtension extensionCancelledByUserError];
+ }
+
+ if (completion) {
+ completion(nil, error);
+ }
+
+ return;
+ }
+
+ [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
+ if (completion) {
+ completion(itemDictionary, error);
+ }
+ }];
+ };
+
+ [viewController presentViewController:activityViewController animated:YES completion:nil];
+#endif
+}
+
+#pragma mark - Web View filling Support
+
+- (void)fillItemIntoWebView:(nonnull id)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+ NSAssert(webView != nil, @"webView must not be nil");
+ NSAssert(viewController != nil, @"viewController must not be nil");
+ NSAssert([webView isKindOfClass:[UIWebView class]] || [webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView or UIWebView.");
+
+#ifdef __IPHONE_8_0
+ if ([webView isKindOfClass:[UIWebView class]]) {
+ [self fillItemIntoUIWebView:webView webViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) {
+ if (completion) {
+ completion(success, error);
+ }
+ }];
+ }
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW
+ else if ([webView isKindOfClass:[WKWebView class]]) {
+ [self fillItemIntoWKWebView:webView forViewController:viewController sender:(id)sender showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) {
+ if (completion) {
+ completion(success, error);
+ }
+ }];
+ }
+ #endif
+#endif
+}
+
+#pragma mark - Support for custom UIActivityViewControllers
+
+- (BOOL)isOnePasswordExtensionActivityType:(nullable NSString *)activityType {
+ return [@"com.agilebits.onepassword-ios.extension" isEqualToString:activityType] || [@"com.agilebits.beta.onepassword-ios.extension" isEqualToString:activityType];
+}
+
+- (void)createExtensionItemForWebView:(nonnull id)webView completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion {
+ NSAssert(webView != nil, @"webView must not be nil");
+ NSAssert([webView isKindOfClass:[UIWebView class]] || [webView isKindOfClass:[WKWebView class]], @"webView must be an instance of WKWebView or UIWebView.");
+
+#ifdef __IPHONE_8_0
+ if ([webView isKindOfClass:[UIWebView class]]) {
+ UIWebView *uiWebView = (UIWebView *)webView;
+ NSString *collectedPageDetails = [uiWebView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript];
+
+ [self createExtensionItemForURLString:uiWebView.request.URL.absoluteString webPageDetails:collectedPageDetails completion:completion];
+ }
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW
+ else if ([webView isKindOfClass:[WKWebView class]]) {
+ WKWebView *wkWebView = (WKWebView *)webView;
+ [wkWebView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *evaluateError) {
+ if (result == nil) {
+ NSLog(@"1Password Extension failed to collect web page fields: %@", evaluateError);
+ NSError *failedToCollectFieldsError = [OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:evaluateError];
+ if (completion) {
+ if ([NSThread isMainThread]) {
+ completion(nil, failedToCollectFieldsError);
+ }
+ else {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(nil, failedToCollectFieldsError);
+ });
+ }
+ }
+
+ return;
+ }
+
+ [self createExtensionItemForURLString:wkWebView.URL.absoluteString webPageDetails:result completion:completion];
+ }];
+ }
+ #endif
+#endif
+}
+
+- (void)fillReturnedItems:(nullable NSArray *)returnedItems intoWebView:(nonnull id)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+ NSAssert(webView != nil, @"webView must not be nil");
+
+ if (returnedItems.count == 0) {
+ NSError *error = [OnePasswordExtension extensionCancelledByUserError];
+ if (completion) {
+ completion(NO, error);
+ }
+
+ return;
+ }
+
+ [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *error) {
+ if (itemDictionary.count == 0) {
+ if (completion) {
+ completion(NO, error);
+ }
+
+ return;
+ }
+
+ NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript];
+ [self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) {
+ if (completion) {
+ completion(success, executeFillScriptError);
+ }
+ }];
+ }];
+}
+
+#pragma mark - Private methods
+
+- (BOOL)isSystemAppExtensionAPIAvailable {
+#ifdef __IPHONE_8_0
+ return [NSExtensionItem class] != nil;
+#else
+ return NO;
+#endif
+}
+
+- (void)findLoginIn1PasswordWithURLString:(nonnull NSString *)URLString collectedPageDetails:(nullable NSString *)collectedPageDetails forWebViewController:(nonnull UIViewController *)forViewController sender:(nullable id)sender withWebView:(nonnull id)webView showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+ if ([URLString length] == 0) {
+ NSError *URLStringError = [OnePasswordExtension failedToObtainURLStringFromWebViewError];
+ NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", URLStringError);
+ if (completion) {
+ completion(NO, URLStringError);
+ }
+ return;
+ }
+
+ NSError *jsonError = nil;
+ NSData *data = [collectedPageDetails dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *collectedPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
+
+ if (collectedPageDetailsDictionary.count == 0) {
+ NSLog(@"Failed to parse JSON collected page details: %@", jsonError);
+ if (completion) {
+ completion(NO, jsonError);
+ }
+ return;
+ }
+
+ NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : collectedPageDetailsDictionary };
+
+ NSString *typeIdentifier = yesOrNo ? kUTTypeAppExtensionFillWebViewAction : kUTTypeAppExtensionFillBrowserAction;
+ UIActivityViewController *activityViewController = [self activityViewControllerForItem:item viewController:forViewController sender:sender typeIdentifier:typeIdentifier];
+ activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
+ if (returnedItems.count == 0) {
+ NSError *error = nil;
+ if (activityError) {
+ NSLog(@"Failed to findLoginIn1PasswordWithURLString: %@", activityError);
+ error = [OnePasswordExtension failedToContactExtensionErrorWithActivityError:activityError];
+ }
+ else {
+ error = [OnePasswordExtension extensionCancelledByUserError];
+ }
+
+ if (completion) {
+ completion(NO, error);
+ }
+
+ return;
+ }
+
+ [self processExtensionItem:returnedItems.firstObject completion:^(NSDictionary *itemDictionary, NSError *processExtensionItemError) {
+ if (itemDictionary.count == 0) {
+ if (completion) {
+ completion(NO, processExtensionItemError);
+ }
+
+ return;
+ }
+
+ NSString *fillScript = itemDictionary[AppExtensionWebViewPageFillScript];
+ [self executeFillScript:fillScript inWebView:webView completion:^(BOOL success, NSError *executeFillScriptError) {
+ if (completion) {
+ completion(success, executeFillScriptError);
+ }
+ }];
+ }];
+ };
+
+ [forViewController presentViewController:activityViewController animated:YES completion:nil];
+}
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW
+- (void)fillItemIntoWKWebView:(nonnull WKWebView *)webView forViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+ [webView evaluateJavaScript:OPWebViewCollectFieldsScript completionHandler:^(NSString *result, NSError *error) {
+ if (result == nil) {
+ NSLog(@"1Password Extension failed to collect web page fields: %@", error);
+ if (completion) {
+ completion(NO,[OnePasswordExtension failedToCollectFieldsErrorWithUnderlyingError:error]);
+ }
+
+ return;
+ }
+
+ [self findLoginIn1PasswordWithURLString:webView.URL.absoluteString collectedPageDetails:result forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *findLoginError) {
+ if (completion) {
+ completion(success, findLoginError);
+ }
+ }];
+ }];
+}
+#endif
+
+- (void)fillItemIntoUIWebView:(nonnull UIWebView *)webView webViewController:(nonnull UIViewController *)viewController sender:(nullable id)sender showOnlyLogins:(BOOL)yesOrNo completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+ NSString *collectedPageDetails = [webView stringByEvaluatingJavaScriptFromString:OPWebViewCollectFieldsScript];
+ [self findLoginIn1PasswordWithURLString:webView.request.URL.absoluteString collectedPageDetails:collectedPageDetails forWebViewController:viewController sender:sender withWebView:webView showOnlyLogins:yesOrNo completion:^(BOOL success, NSError *error) {
+ if (completion) {
+ completion(success, error);
+ }
+ }];
+}
+
+- (void)executeFillScript:(NSString * __nullable)fillScript inWebView:(nonnull id)webView completion:(nonnull OnePasswordSuccessCompletionBlock)completion {
+
+ if (fillScript == nil) {
+ NSLog(@"Failed to executeFillScript, fillScript is missing");
+ if (completion) {
+ completion(NO, [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script is missing", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:nil]);
+ }
+
+ return;
+ }
+
+ NSMutableString *scriptSource = [OPWebViewFillScript mutableCopy];
+ [scriptSource appendFormat:@"(document, %@, undefined);", fillScript];
+
+#ifdef __IPHONE_8_0
+ if ([webView isKindOfClass:[UIWebView class]]) {
+ NSString *result = [((UIWebView *)webView) stringByEvaluatingJavaScriptFromString:scriptSource];
+ BOOL success = (result != nil);
+ NSError *error = nil;
+
+ if (!success) {
+ NSLog(@"Cannot executeFillScript, stringByEvaluatingJavaScriptFromString failed");
+ error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script could not be evaluated", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:nil];
+ }
+
+ if (completion) {
+ completion(success, error);
+ }
+ }
+
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 || ONE_PASSWORD_EXTENSION_ENABLE_WK_WEB_VIEW
+ else if ([webView isKindOfClass:[WKWebView class]]) {
+ [((WKWebView *)webView) evaluateJavaScript:scriptSource completionHandler:^(NSString *result, NSError *evaluationError) {
+ BOOL success = (result != nil);
+ NSError *error = nil;
+
+ if (!success) {
+ NSLog(@"Cannot executeFillScript, evaluateJavaScript failed: %@", evaluationError);
+ error = [OnePasswordExtension failedToFillFieldsErrorWithLocalizedErrorMessage:NSLocalizedStringFromTable(@"Failed to fill web page because script could not be evaluated", @"OnePasswordExtension", @"1Password Extension Error Message") underlyingError:error];
+ }
+
+ if (completion) {
+ completion(success, error);
+ }
+ }];
+ }
+ #endif
+#endif
+}
+
+#ifdef __IPHONE_8_0
+- (void)processExtensionItem:(nullable NSExtensionItem *)extensionItem completion:(nonnull OnePasswordLoginDictionaryCompletionBlock)completion {
+ if (extensionItem.attachments.count == 0) {
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item had no attachments." };
+ NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo];
+ if (completion) {
+ completion(nil, error);
+ }
+ return;
+ }
+
+ NSItemProvider *itemProvider = extensionItem.attachments.firstObject;
+ if (NO == [itemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypePropertyList]) {
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Unexpected data returned by App Extension: extension item attachment does not conform to kUTTypePropertyList type identifier" };
+ NSError *error = [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeUnexpectedData userInfo:userInfo];
+ if (completion) {
+ completion(nil, error);
+ }
+ return;
+ }
+
+
+ [itemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *itemDictionary, NSError *itemProviderError) {
+ NSError *error = nil;
+ if (itemDictionary.count == 0) {
+ NSLog(@"Failed to loadItemForTypeIdentifier: %@", itemProviderError);
+ error = [OnePasswordExtension failedToLoadItemProviderDataErrorWithUnderlyingError:itemProviderError];
+ }
+
+ if (completion) {
+ if ([NSThread isMainThread]) {
+ completion(itemDictionary, error);
+ }
+ else {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(itemDictionary, error);
+ });
+ }
+ }
+ }];
+}
+
+- (UIActivityViewController *)activityViewControllerForItem:(nonnull NSDictionary *)item viewController:(nonnull UIViewController*)viewController sender:(nullable id)sender typeIdentifier:(nonnull NSString *)typeIdentifier {
+#ifdef __IPHONE_8_0
+ NSAssert(NO == (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && sender == nil), @"sender must not be nil on iPad.");
+
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:typeIdentifier];
+
+ NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
+ extensionItem.attachments = @[ itemProvider ];
+
+ UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[ extensionItem ] applicationActivities:nil];
+
+ if ([sender isKindOfClass:[UIBarButtonItem class]]) {
+ controller.popoverPresentationController.barButtonItem = sender;
+ }
+ else if ([sender isKindOfClass:[UIView class]]) {
+ controller.popoverPresentationController.sourceView = [sender superview];
+ controller.popoverPresentationController.sourceRect = [sender frame];
+ }
+ else {
+ NSLog(@"sender can be nil on iPhone");
+ }
+
+ return controller;
+#else
+ return nil;
+#endif
+}
+
+#endif
+
+- (void)createExtensionItemForURLString:(nonnull NSString *)URLString webPageDetails:(nullable NSString *)webPageDetails completion:(nonnull OnePasswordExtensionItemCompletionBlock)completion {
+ NSError *jsonError = nil;
+ NSData *data = [webPageDetails dataUsingEncoding:NSUTF8StringEncoding];
+ NSDictionary *webPageDetailsDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&jsonError];
+
+ if (webPageDetailsDictionary.count == 0) {
+ NSLog(@"Failed to parse JSON collected page details: %@", jsonError);
+ if (completion) {
+ completion(nil, jsonError);
+ }
+ return;
+ }
+
+ NSDictionary *item = @{ AppExtensionVersionNumberKey : VERSION_NUMBER, AppExtensionURLStringKey : URLString, AppExtensionWebViewPageDetails : webPageDetailsDictionary };
+
+ NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:kUTTypeAppExtensionFillBrowserAction];
+
+ NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
+ extensionItem.attachments = @[ itemProvider ];
+
+ if (completion) {
+ if ([NSThread isMainThread]) {
+ completion(extensionItem, nil);
+ }
+ else {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ completion(extensionItem, nil);
+ });
+ }
+ }
+}
+
+#pragma mark - Errors
+
++ (NSError *)systemAppExtensionAPINotAvailableError {
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"App Extension API is not available in this version of iOS", @"OnePasswordExtension", @"1Password Extension Error Message") };
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeAPINotAvailable userInfo:userInfo];
+}
+
+
++ (NSError *)extensionCancelledByUserError {
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"1Password Extension was cancelled by the user", @"OnePasswordExtension", @"1Password Extension Error Message") };
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCancelledByUser userInfo:userInfo];
+}
+
++ (NSError *)failedToContactExtensionErrorWithActivityError:(nullable NSError *)activityError {
+ NSMutableDictionary *userInfo = [NSMutableDictionary new];
+ userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to contact the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message");
+ if (activityError) {
+ userInfo[NSUnderlyingErrorKey] = activityError;
+ }
+
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToContactExtension userInfo:userInfo];
+}
+
++ (NSError *)failedToCollectFieldsErrorWithUnderlyingError:(nullable NSError *)underlyingError {
+ NSMutableDictionary *userInfo = [NSMutableDictionary new];
+ userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to execute script that collects web page information", @"OnePasswordExtension", @"1Password Extension Error Message");
+ if (underlyingError) {
+ userInfo[NSUnderlyingErrorKey] = underlyingError;
+ }
+
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeCollectFieldsScriptFailed userInfo:userInfo];
+}
+
++ (NSError *)failedToFillFieldsErrorWithLocalizedErrorMessage:(nullable NSString *)errorMessage underlyingError:(nullable NSError *)underlyingError {
+ NSMutableDictionary *userInfo = [NSMutableDictionary new];
+ if (errorMessage) {
+ userInfo[NSLocalizedDescriptionKey] = errorMessage;
+ }
+ if (underlyingError) {
+ userInfo[NSUnderlyingErrorKey] = underlyingError;
+ }
+
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFillFieldsScriptFailed userInfo:userInfo];
+}
+
++ (NSError *)failedToLoadItemProviderDataErrorWithUnderlyingError:(nullable NSError *)underlyingError {
+ NSMutableDictionary *userInfo = [NSMutableDictionary new];
+ userInfo[NSLocalizedDescriptionKey] = NSLocalizedStringFromTable(@"Failed to parse information returned by 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message");
+ if (underlyingError) {
+ userInfo[NSUnderlyingErrorKey] = underlyingError;
+ }
+
+ return [[NSError alloc] initWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToLoadItemProviderData userInfo:userInfo];
+}
+
++ (NSError *)failedToObtainURLStringFromWebViewError {
+ NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : NSLocalizedStringFromTable(@"Failed to obtain URL String from web view. The web view must be loaded completely when calling the 1Password Extension", @"OnePasswordExtension", @"1Password Extension Error Message") };
+ return [NSError errorWithDomain:AppExtensionErrorDomain code:AppExtensionErrorCodeFailedToObtainURLStringFromWebView userInfo:userInfo];
+}
+
+#pragma mark - WebView field collection and filling scripts
+
+static NSString *const OPWebViewCollectFieldsScript = @";(function(document, undefined) {\
+var isFirefox = false, isChrome = false, isSafari = true;\
+\
+ document.elementsByOPID={};document.addEventListener('input',function(c){!1!==c.a&&'input'===c.target.tagName.toLowerCase()&&(c.target.dataset['com.agilebits.onepassword.userEdited']='yes')},!0);\
+function q(c,d){function b(a,c){var e=a[c];if('string'==typeof e)return e;e=a.getAttribute(c);return'string'==typeof e?e:null}function g(a,c){if(-1===['text','password'].indexOf(c.type.toLowerCase())||!(m.test(a.value)||m.test(a.htmlID)||m.test(a.htmlName)||m.test(a.placeholder)||m.test(a['label-tag'])||m.test(a['label-data'])||m.test(a['label-aria'])))return!1;if(!a.visible)return!0;if('password'==c.type.toLowerCase())return!1;var e=c.type;u(c,!0);return e!==c.type}function n(a){switch(p(a.type)){case 'checkbox':return a.checked?\
+'✓':'';case 'hidden':a=a.value;if(!a||'number'!=typeof a.length)return'';254\\/?]/mg,''):null;return[c?c:null,a.value]}),{options:a}):null}function r(a){var c;for(a=a.parentElement||\
+a.parentNode;a&&'td'!=p(a.tagName);)a=a.parentElement||a.parentNode;if(!a||void 0===a)return null;c=a.parentElement||a.parentNode;if('tr'!=c.tagName.toLowerCase())return null;c=c.previousElementSibling;if(!c||'tr'!=(c.tagName+'').toLowerCase()||c.cells&&a.cellIndex>=c.cells.length)return null;a=y(c.cells[a.cellIndex]);return a=v(a)}function s(a){var b,e=[];if(a.labels&&a.labels.length&&0c.clientWidth||10>c.clientHeight)return!1;var s=c.getClientRects();if(0===s.length)return!1;for(var f=0;fg||0>r.right)return!1;if(0>l||l>g||0>d||d>n)return!1;for(b=c.ownerDocument.elementFromPoint(l+(b.right>window.innerWidth?(window.innerWidth-l)/2:b.width/2),d+(b.bottom>window.innerHeight?\
+(window.innerHeight-d)/2:b.height/2));b&&b!==c&&b!==document;){if(b.tagName&&'string'===typeof b.tagName&&'label'===b.tagName.toLowerCase()&&c.labels&&0 [String: Any] {
+ // Excellent
+ return [ AppExtensionGeneratedPasswordMinLengthKey: "10",
+ AppExtensionGeneratedPasswordMaxLengthKey: "128",
+ AppExtensionGeneratedPasswordRequireDigitsKey: true,
+ AppExtensionGeneratedPasswordRequireSymbolsKey: true ]
+ }
+}
diff --git a/Lock/Style.swift b/Lock/Style.swift
index cc82e7f02..cc85f0788 100644
--- a/Lock/Style.swift
+++ b/Lock/Style.swift
@@ -127,6 +127,9 @@ public struct Style {
/// Passwordless search bar style
public var searchBarStyle: UISearchBarStyle = .default
+ /// 1Password Icon color
+ public var onePasswordIconColor = UIColor(red: 0.5725, green: 0.5804, blue: 0.5843, alpha: 1.0)
+
var headerMask: UIImage? {
let image = self.logo.image(compatibleWithTraits: nil)
if Style.Auth0.logo == self.logo {
diff --git a/LockTests/Interactors/PasswordlessInteractorSpec.swift b/LockTests/Interactors/PasswordlessInteractorSpec.swift
index 56e177c74..f6973d39d 100644
--- a/LockTests/Interactors/PasswordlessInteractorSpec.swift
+++ b/LockTests/Interactors/PasswordlessInteractorSpec.swift
@@ -250,7 +250,7 @@ class PasswordlessInteractorSpec: QuickSpec {
}
it("should store passwordless transaction on sending link") {
- waitUntil(timeout: 2) { done in
+ waitUntil(timeout: 4) { done in
interactor.request(connection.name) { error in
expect(error).to(beNil())
done()
diff --git a/LockTests/Models/PasswordPolicySpec.swift b/LockTests/Models/PasswordPolicySpec.swift
index 0f86b1338..b60c3fd81 100644
--- a/LockTests/Models/PasswordPolicySpec.swift
+++ b/LockTests/Models/PasswordPolicySpec.swift
@@ -119,6 +119,36 @@ class PasswordPolicySpec: QuickSpec {
}
+ describe("one password policy recipe") {
+
+ it("none should return excellent") {
+ let policy = PasswordPolicy.none
+ expect(policy.onePasswordRules()).to(beExcellentPassword())
+ }
+
+ it("low should return excellent") {
+ let policy = PasswordPolicy.low
+ expect(policy.onePasswordRules()).to(beExcellentPassword())
+ }
+
+ it("fair should return excellent") {
+ let policy = PasswordPolicy.fair
+ expect(policy.onePasswordRules()).to(beExcellentPassword())
+ }
+
+ it("good should return excellent") {
+ let policy = PasswordPolicy.good
+ expect(policy.onePasswordRules()).to(beExcellentPassword())
+ }
+
+ it("excellent should return excellent") {
+ let policy = PasswordPolicy.excellent
+ expect(policy.onePasswordRules()).to(beExcellentPassword())
+ }
+
+ }
+
+
}
}
diff --git a/LockTests/OnePasswordSpec.swift b/LockTests/OnePasswordSpec.swift
new file mode 100644
index 000000000..adad24f92
--- /dev/null
+++ b/LockTests/OnePasswordSpec.swift
@@ -0,0 +1,76 @@
+// OnePasswordSpec.swift
+//
+// Copyright (c) 2017 Auth0 (http://auth0.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Quick
+import Nimble
+
+@testable import Lock
+
+class OnePasswordSpec: QuickSpec {
+
+ override func spec() {
+
+ var onePassword: OnePassword?
+ var viewController: MockViewController?
+
+ describe("init") {
+
+ beforeEach {
+ onePassword = nil
+ }
+
+ it("should init with defaults") {
+ onePassword = OnePassword()
+ expect(onePassword?.appIdentifier) == Bundle.main.bundleIdentifier!
+ expect(onePassword?.displayName) == Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
+ expect(onePassword?.enabled) == true
+ }
+
+ it("should not be available without 1password installed") {
+ onePassword = OnePassword()
+ expect(onePassword?.available) == false
+ }
+
+ }
+
+ describe("action") {
+
+ beforeEach {
+ viewController = MockViewController()
+ onePassword = OnePassword()
+ onePassword?.controller = viewController
+ }
+
+ it("should present extension prompt on login") {
+ onePassword?.login { _ in }
+ expect(viewController?.presented).toNot(beNil())
+ }
+
+
+ it("should present extension prompt on store") {
+ onePassword?.store(withPolicy: nil, identifier: "username") { _ in }
+ expect(viewController?.presented).toNot(beNil())
+ }
+
+ }
+ }
+}
diff --git a/LockTests/OptionsSpec.swift b/LockTests/OptionsSpec.swift
index 2065883ba..ddc4cb6da 100644
--- a/LockTests/OptionsSpec.swift
+++ b/LockTests/OptionsSpec.swift
@@ -106,6 +106,17 @@ class OptionsSpec: QuickSpec {
expect(options.passwordlessMethod).to(equal(PasswordlessMethod.code))
}
+ it("should have passwordManager enabled") {
+ expect(options.passwordManager.enabled) == true
+ }
+
+ it("should have passwordManager app bundler identifier") {
+ expect(options.passwordManager.appIdentifier) == Bundle.main.bundleIdentifier!
+ }
+
+ it("should have passwordManager app display name") {
+ expect(options.passwordManager.displayName) == Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
+ }
}
describe("validation") {
diff --git a/LockTests/PasswordPolicyValidatorSpec.swift b/LockTests/PasswordPolicyValidatorSpec.swift
index bde6271ed..02ac0871d 100644
--- a/LockTests/PasswordPolicyValidatorSpec.swift
+++ b/LockTests/PasswordPolicyValidatorSpec.swift
@@ -64,7 +64,6 @@ class PasswordPolicyValidatorSpec: QuickSpec {
fail("Invalid error. Expected PasswordPolicyViolation")
}
}
-
}
}
diff --git a/LockTests/Presenters/DatabasePresenterSpec.swift b/LockTests/Presenters/DatabasePresenterSpec.swift
index 253824318..483a34f53 100644
--- a/LockTests/Presenters/DatabasePresenterSpec.swift
+++ b/LockTests/Presenters/DatabasePresenterSpec.swift
@@ -39,12 +39,14 @@ class DatabasePresenterSpec: QuickSpec {
var navigator: MockNavigator!
var options: OptionBuildable!
var user: User!
+ var passwordManager: MockPasswordManager!
beforeEach {
oauth2 = MockOAuth2()
connections = OfflineConnections()
options = LockOptions()
user = User()
+ passwordManager = MockPasswordManager()
enterpriseInteractor = EnterpriseDomainInteractor(connections: connections, user: user, authentication: oauth2)
authPresenter = MockAuthPresenter(connections: [], interactor: MockAuthInteractor(), customStyle: [:])
messagePresenter = MockMessagePresenter()
@@ -215,6 +217,28 @@ class DatabasePresenterSpec: QuickSpec {
expect(view.primaryButton?.title) == "LOG IN"
}
+ it("should not show password manager button") {
+ expect(view.passwordManagerButton).to(beNil())
+ }
+
+ context("with password manager available") {
+
+ beforeEach {
+ presenter.passwordManager = passwordManager
+ view = presenter.view as! DatabaseOnlyView
+ }
+
+ it("should show password manager button") {
+ expect(view.passwordManagerButton).toNot(beNil())
+ }
+
+ it("should not show password manager when disabled") {
+ presenter.passwordManager.enabled = false
+ view = presenter.view as! DatabaseOnlyView
+ expect(view.passwordManagerButton).to(beNil())
+ }
+ }
+
describe("user input") {
it("should clear global message") {
@@ -380,8 +404,31 @@ class DatabasePresenterSpec: QuickSpec {
expect(messagePresenter.message).toEventuallyNot(beNil())
}
}
+
+ context("password manager") {
+
+ var username: String?
+ var password: String?
+
+ beforeEach {
+ username = nil
+ password = nil
+ presenter.passwordManager = passwordManager
+ view = presenter.view as! DatabaseOnlyView
+ view.switcher?.selected = .signup
+ view.switcher?.onSelectionChange(view.switcher!)
+ presenter.passwordManager.onUpdate = { username = $0; password = $1 }
+ }
+
+ it("should trigger password manager and return username and password") {
+ view.passwordManagerButton?.pressed(view.passwordManagerButton!)
+ expect(username).toNot(beNil())
+ expect(password).toNot(beNil())
+ }
+ }
}
+
// MARK: - Sign Up
describe("sign up") {
@@ -453,6 +500,40 @@ class DatabasePresenterSpec: QuickSpec {
expect(input.valid) == false
}
+ it("should not show password manager button") {
+ expect(view.passwordManagerButton).to(beNil())
+ }
+
+ context("with password manager available") {
+
+ beforeEach {
+ presenter.passwordManager = passwordManager
+ view = presenter.view as! DatabaseOnlyView
+ view.switcher?.selected = .signup
+ view.switcher?.onSelectionChange(view.switcher!)
+ }
+
+ it("should show password manager button") {
+ expect(view.passwordManagerButton).toNot(beNil())
+ }
+
+ context("disable password manager") {
+
+ beforeEach {
+ presenter.passwordManager = passwordManager
+ presenter.passwordManager.enabled = false
+ view = presenter.view as! DatabaseOnlyView
+ view.switcher?.selected = .signup
+ view.switcher?.onSelectionChange(view.switcher!)
+ }
+
+ it("should not show password manager when disabled") {
+ expect(view.passwordManagerButton).to(beNil())
+ }
+
+ }
+ }
+
}
describe("sign up action") {
@@ -593,27 +674,49 @@ class DatabasePresenterSpec: QuickSpec {
expect(button.title).toEventually(contain("SIGN UP"))
}
- context("no auto login or auto close") {
+ context("no auto login or auto close") {
+
+ beforeEach {
+ options.loginAfterSignup = false
+ options.allow = [.Signup]
+ options.autoClose = false
+ presenter = DatabasePresenter(authenticator: interactor, creator: interactor, connection: DatabaseConnection(name: connection, requiresUsername: true), navigator: navigator, options: options)
+ presenter.messagePresenter = messagePresenter
+ view = presenter.view as! DatabaseOnlyView
+ }
+
+ it("should show no success message") {
+ interactor.onSignUp = {
+ return nil
+ }
+ view.primaryButton?.onPress(view.primaryButton!)
+ expect(messagePresenter.error).toEventually(beNil())
+ expect(messagePresenter.message).toEventuallyNot(beNil())
+ }
+ }
+ }
+
+ describe("password manager action") {
+
+ var username: String?
+ var password: String?
beforeEach {
- options.loginAfterSignup = false
- options.allow = [.Signup]
- options.autoClose = false
- presenter = DatabasePresenter(authenticator: interactor, creator: interactor, connection: DatabaseConnection(name: connection, requiresUsername: true), navigator: navigator, options: options)
- presenter.messagePresenter = messagePresenter
+ username = nil
+ password = nil
+ presenter.passwordManager = passwordManager
view = presenter.view as! DatabaseOnlyView
+ view.switcher?.selected = .signup
+ view.switcher?.onSelectionChange(view.switcher!)
+ presenter.passwordManager.onUpdate = { username = $0; password = $1 }
}
- it("should show no success message") {
- interactor.onSignUp = {
- return nil
- }
- view.primaryButton?.onPress(view.primaryButton!)
- expect(messagePresenter.error).toEventually(beNil())
- expect(messagePresenter.message).toEventuallyNot(beNil())
+ it("should trigger password manager and return username and password") {
+ view.passwordManagerButton?.pressed(view.passwordManagerButton!)
+ expect(username).toNot(beNil())
+ expect(password).toNot(beNil())
}
}
- }
}
describe("tos action") {
@@ -729,32 +832,32 @@ class DatabasePresenterSpec: QuickSpec {
beforeEach {
var options = LockOptions()
options.enterpriseConnectionUsingActiveAuth = ["validAD"]
-
+
connections = OfflineConnections()
connections.enterprise(name: "validAD", domains: ["valid.com"])
-
+
presenter = DatabasePresenter(authenticator: interactor, creator: interactor, connection: DatabaseConnection(name: connection, requiresUsername: true), navigator: navigator, options: options)
enterpriseInteractor = EnterpriseDomainInteractor(connections: connections, user: user, authentication: oauth2)
presenter.enterpriseInteractor = enterpriseInteractor
-
+
view = presenter.view as! DatabaseOnlyView
-
+
let input = mockInput(.email, value: "user@valid.com")
view.form?.onValueChange(input)
-
+
}
-
+
it("should navigate to enterprise password presenter") {
view.primaryButton?.onPress(view.primaryButton!)
let connection = presenter.enterpriseInteractor?.connection!
expect(connection).toNot(beNil())
expect(navigator.route).toEventually(equal(Route.enterpriseActiveAuth(connection: connection!, domain: "valid.com")))
}
-
+
}
-
+
}
-
+
}
}
}
diff --git a/LockTests/StyleSpec.swift b/LockTests/StyleSpec.swift
index c3b44cd2d..843d4d1c1 100644
--- a/LockTests/StyleSpec.swift
+++ b/LockTests/StyleSpec.swift
@@ -124,6 +124,11 @@ class StyleSpec: QuickSpec {
it("should have header back button image") {
expect(style.headerBackIcon) == lazyImage(named: "ic_back")
}
+
+ it("should have header close button image") {
+ expect(style.onePasswordIconColor) == UIColor(red: 0.5725, green: 0.5804, blue: 0.5843, alpha: 1.0)
+ }
+
}
describe("custom style") {
diff --git a/LockTests/Utils/Matchers.swift b/LockTests/Utils/Matchers.swift
index 1eef0a587..297b04222 100644
--- a/LockTests/Utils/Matchers.swift
+++ b/LockTests/Utils/Matchers.swift
@@ -63,3 +63,18 @@ func beErrorResult() -> MatcherFunc {
return false
}
}
+
+func beExcellentPassword() -> MatcherFunc<[String: Any]> {
+ return MatcherFunc { expression, failureMessage in
+ failureMessage.postfixMessage = "be an excellent strength password recipe"
+ if let actual = try expression.evaluate(),
+ actual[AppExtensionGeneratedPasswordMinLengthKey] as? String == "10",
+ actual[AppExtensionGeneratedPasswordMaxLengthKey] as? String == "128",
+ actual[AppExtensionGeneratedPasswordRequireDigitsKey] as? Bool == true,
+ actual[AppExtensionGeneratedPasswordRequireSymbolsKey] as? Bool == true
+ {
+ return true
+ }
+ return false
+ }
+}
diff --git a/LockTests/Utils/Mocks.swift b/LockTests/Utils/Mocks.swift
index 89f784170..29e37720e 100644
--- a/LockTests/Utils/Mocks.swift
+++ b/LockTests/Utils/Mocks.swift
@@ -289,6 +289,10 @@ class MockAuthentication: Authentication {
func loginSocial(token: String, connection: String, scope: String, parameters: [String : Any]) -> Request {
return self.authentication.loginSocial(token: token, connection: connection, scope: scope, parameters: parameters)
}
+
+ func revoke(refreshToken: String) -> Request {
+ return self.revoke(refreshToken: refreshToken)
+ }
}
class MockWebAuth: WebAuth {
@@ -493,3 +497,28 @@ class MockPasswordlessInteractor: PasswordlessAuthenticatable {
callback(onLogin())
}
}
+
+class MockPasswordManager: PasswordManager {
+
+ var identifier: String = "username"
+ var password: String = "password"
+
+ var _available: Bool = true
+ var enabled: Bool = true
+
+ var available: Bool {
+ return self.enabled && _available
+ }
+
+ var onUpdate: (String, String) -> Void = { _ in }
+
+ func login(callback: @escaping (Error?) -> Void) {
+ self.onUpdate(identifier, self.password)
+ callback(nil)
+ }
+
+ func store(withPolicy policy: [String: Any]?, identifier: String?, callback: @escaping (Error?) -> Void) {
+ self.onUpdate(self.identifier, self.password)
+ callback(nil)
+ }
+}
diff --git a/LockSnapshot/Info.plist b/LockUITests/Info.plist
similarity index 100%
rename from LockSnapshot/Info.plist
rename to LockUITests/Info.plist
diff --git a/LockSnapshot/LockSnapshot.swift b/LockUITests/LockUITests.swift
similarity index 98%
rename from LockSnapshot/LockSnapshot.swift
rename to LockUITests/LockUITests.swift
index e926646d8..e57b8db94 100644
--- a/LockSnapshot/LockSnapshot.swift
+++ b/LockUITests/LockUITests.swift
@@ -1,4 +1,4 @@
-// LockSnapshot.swift
+// LockUITests.swift
//
// Copyright (c) 2017 Auth0 (http://auth0.com)
//
@@ -23,7 +23,7 @@
import XCTest
@testable import Lock
-class LockSnapshot: XCTestCase {
+class LockUITests: XCTestCase {
let app = XCUIApplication()
diff --git a/LockSnapshot/SnapshotHelper.swift b/LockUITests/SnapshotHelper.swift
similarity index 100%
rename from LockSnapshot/SnapshotHelper.swift
rename to LockUITests/SnapshotHelper.swift
diff --git a/README.md b/README.md
index 692b43f98..a1aada4a6 100644
--- a/README.md
+++ b/README.md
@@ -364,6 +364,34 @@ When signing up the default information requirements are the user's *email* and
*Note: You must specify the icon to use with your custom text field and store it in your App's bundle.*
+#### Password Manager
+
+By default password manager support using [1Password](https://1password.com/) is enabled for database connections, although you will still need to have the 1Password app installed for the option to be visible in the login and signup screens. You can disable 1Password support using the `enabled` property of the `passwordManager`.
+
+```swift
+.withOptions {
+ $0.passwordManager.enabled = false
+}
+```
+
+By default the `appIdentifier` will be set to the app's bundle identifier and the `displayName` will be set to the app's display name. You can customize these as follows:
+
+```swift
+.withOptions {
+ $0.passwordManager.appIdentifier = "www.myapp.com"
+ $0.passwordManager.displayName = "My App"
+}
+```
+
+You will need to add the following to your app's `info.plist`:
+
+```xml
+LSApplicationQueriesSchemes
+
+ org-appextension-feature-password-management
+
+```
+
#### Enterprise
* **enterpriseConnectionUsingActiveAuth**: By default Enterprise connections will use Web Authentication. However you can specify which connections will alternatively use credential authentication and prompt for a username and password.
diff --git a/codecov.yml b/codecov.yml
index 883c8ef4d..c88564d32 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -3,6 +3,7 @@ coverage:
round: down
range: "70...100"
ignore:
+ - Lock/OnePasswordExtension.*
- LockTests/*
- App/*
status: