Merge branch 'release/0.3.1'
This commit is contained in:
commit
620e11be46
27 changed files with 584 additions and 260 deletions
5
Cartfile
5
Cartfile
|
|
@ -1,8 +1,7 @@
|
|||
github "SVProgressHUD/SVProgressHUD"
|
||||
github "radex/SwiftyUserDefaults"
|
||||
github "libgit2/objective-git"
|
||||
# github "zahlz/SwiftPasscodeLock" "master"
|
||||
github "yishilin14/SwiftPasscodeLock" "app-extension-support"
|
||||
github "bitserf/FavIcon"
|
||||
github "leonbreedt/FavIcon"
|
||||
github "kishikawakatsumi/KeychainAccess"
|
||||
github "mattrubin/OneTimePassword"
|
||||
github "jpsim/Yams"
|
||||
|
|
|
|||
2
Podfile
2
Podfile
|
|
@ -2,7 +2,7 @@ platform :ios, '10.2'
|
|||
use_frameworks!
|
||||
|
||||
target 'passKit' do
|
||||
pod 'ObjectivePGP', :git => 'https://github.com/krzyzanowskim/ObjectivePGP.git', :tag => '0.10.0-beta2'
|
||||
pod 'ObjectivePGP', :git => 'https://github.com/krzyzanowskim/ObjectivePGP.git', :tag => '0.10.0'
|
||||
target 'pass' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<img src="icon/icon_round.png" width="76"/>
|
||||
|
||||
# Pass
|
||||
[](https://github.com/mssun/pass-ios/releases)
|
||||
[](https://github.com/mssun/passforios/releases)
|
||||

|
||||
[](https://gitter.im/passforios/passforios)
|
||||
[](https://travis-ci.org/mssun/passforios)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ platform :ios do
|
|||
)
|
||||
# ensure_git_status_clean
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number(version: get_version_number, initial_build_number: 0) + 1,
|
||||
build_number: latest_testflight_build_number(version: get_version_number(target: "pass"), initial_build_number: 0) + 1,
|
||||
xcodeproj: "pass.xcodeproj"
|
||||
)
|
||||
# commit_version_bump(xcodeproj: "pass.xcodeproj")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */; };
|
||||
23B82F0228254275DBA609E7 /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B975797E0F0B7476CADD6A7D /* Pods_passExtension.framework */; };
|
||||
3012B06D2039D6E400BE1793 /* Yams.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3012B06C2039D6E400BE1793 /* Yams.framework */; };
|
||||
61326CDA7A73757FB68DCB04 /* Pods_passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAB3F5541E51ADC8C6B56642 /* Pods_passKit.framework */; };
|
||||
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
|
||||
A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */; };
|
||||
|
|
@ -29,8 +30,6 @@
|
|||
A26700371EEC475600176B8A /* passProcessor.js in Resources */ = {isa = PBXBuildFile; fileRef = A26700351EEC475600176B8A /* passProcessor.js */; };
|
||||
A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; };
|
||||
A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2802BF81E70813A00879216 /* SliderTableViewCell.xib */; };
|
||||
A28C66651EF109D600A398A1 /* PasscodeLockConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28C66631EF109D600A398A1 /* PasscodeLockConfiguration.swift */; };
|
||||
A28C66661EF109D600A398A1 /* PasscodeLockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28C66641EF109D600A398A1 /* PasscodeLockRepository.swift */; };
|
||||
A28C66681EF10EC900A398A1 /* PasscodeExtensionDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */; };
|
||||
A2A61C131EEF90CB00CFE063 /* Base32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A262A58C1E68749C006B0890 /* Base32.framework */; };
|
||||
A2A61C151EEF90CB00CFE063 /* KeychainAccess.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCA742D91E599ED400D54E16 /* KeychainAccess.framework */; };
|
||||
|
|
@ -39,6 +38,10 @@
|
|||
A2A61C201EEFABAD00CFE063 /* UtilsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C1F1EEFABAD00CFE063 /* UtilsExtension.swift */; };
|
||||
A2A61C2C1EEFDF3300CFE063 /* ExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */; };
|
||||
A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; };
|
||||
A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */; };
|
||||
A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */; };
|
||||
A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */; };
|
||||
A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */; };
|
||||
A2F4E2141EED800F0011986E /* GitCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F4E2101EED800F0011986E /* GitCredential.swift */; };
|
||||
A2F4E2151EED800F0011986E /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F4E2111EED800F0011986E /* Password.swift */; };
|
||||
A2F4E2161EED800F0011986E /* PasswordEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2F4E2121EED800F0011986E /* PasswordEntity.swift */; };
|
||||
|
|
@ -61,7 +64,6 @@
|
|||
DC037CC01E4ED4E100609409 /* TextViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC037CBE1E4ED4E100609409 /* TextViewTableViewCell.xib */; };
|
||||
DC13B1511E8640810097803F /* passTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13B1501E8640810097803F /* passTests.swift */; };
|
||||
DC193FFA1E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC193FF91E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift */; };
|
||||
DC193FFC1E49E0340077E0A3 /* PasscodeLock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC193FFB1E49E0340077E0A3 /* PasscodeLock.framework */; };
|
||||
DC3E64E61E656F11009A83DE /* CommitLogsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */; };
|
||||
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
|
||||
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
|
||||
|
|
@ -157,6 +159,7 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3012B06C2039D6E400BE1793 /* Yams.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Yams.framework; path = Carthage/Build/iOS/Yams.framework; sourceTree = "<group>"; };
|
||||
31C3033E8868D05B2C55C8B1 /* Pods-passExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3A5620D17DF5E86B61761D0E /* Pods_pass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_pass.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
666769E0B255666D02945C15 /* Pods-passKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-passKitTests/Pods-passKitTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
|
@ -191,8 +194,6 @@
|
|||
A26700351EEC475600176B8A /* passProcessor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = passProcessor.js; sourceTree = "<group>"; };
|
||||
A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = "<group>"; };
|
||||
A28C66631EF109D600A398A1 /* PasscodeLockConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockConfiguration.swift; path = Models/PasscodeLockConfiguration.swift; sourceTree = "<group>"; };
|
||||
A28C66641EF109D600A398A1 /* PasscodeLockRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockRepository.swift; path = Models/PasscodeLockRepository.swift; sourceTree = "<group>"; };
|
||||
A28C66671EF10EC900A398A1 /* PasscodeExtensionDisplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeExtensionDisplay.swift; sourceTree = "<group>"; };
|
||||
A2A61C0C1EEF8DFE00CFE063 /* libPods-passExtension.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passExtension.a"; path = "../../Library/Developer/Xcode/DerivedData/pass-fwlmfsjroyvbfhdyqmglrwfhvjli/Build/Products/Debug-iphonesimulator/libPods-passExtension.a"; sourceTree = "<group>"; };
|
||||
A2A61C101EEF8E3500CFE063 /* libPods-passKit.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-passKit.a"; path = "Pods/../build/Debug-iphoneos/libPods-passKit.a"; sourceTree = "<group>"; };
|
||||
|
|
@ -200,6 +201,10 @@
|
|||
A2A61C2B1EEFDF3300CFE063 /* ExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionViewController.swift; sourceTree = "<group>"; };
|
||||
A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = "<group>"; };
|
||||
A2BC54C71EEE5669001FAFBD /* Objective-CBridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Objective-CBridgingHeader.h"; sourceTree = "<group>"; };
|
||||
A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIViewExtension.swift; path = Helpers/UIViewExtension.swift; sourceTree = "<group>"; };
|
||||
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLock.swift; path = Models/PasscodeLock.swift; sourceTree = "<group>"; };
|
||||
A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockViewController.swift; path = Controllers/PasscodeLockViewController.swift; sourceTree = "<group>"; };
|
||||
A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasscodeLockPresenter.swift; path = Controllers/PasscodeLockPresenter.swift; sourceTree = "<group>"; };
|
||||
A2F4E2101EED800F0011986E /* GitCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GitCredential.swift; path = Models/GitCredential.swift; sourceTree = "<group>"; };
|
||||
A2F4E2111EED800F0011986E /* Password.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Password.swift; path = Models/Password.swift; sourceTree = "<group>"; };
|
||||
A2F4E2121EED800F0011986E /* PasswordEntity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasswordEntity.swift; path = Models/PasswordEntity.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -315,8 +320,8 @@
|
|||
files = (
|
||||
A260758D1EEC6F34005DB03E /* passKit.framework in Frameworks */,
|
||||
DCC408C71E307DBB00F29B0E /* SVProgressHUD.framework in Frameworks */,
|
||||
DC193FFC1E49E0340077E0A3 /* PasscodeLock.framework in Frameworks */,
|
||||
18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */,
|
||||
3012B06D2039D6E400BE1793 /* Yams.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -344,11 +349,12 @@
|
|||
A26075791EEC6F34005DB03E /* passKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A2C532B9201DD07500DB9F53 /* Controllers */,
|
||||
A2F4E20F1EED7F0A0011986E /* Helpers */,
|
||||
A260757B1EEC6F34005DB03E /* Info.plist */,
|
||||
A2F4E20E1EED7F040011986E /* Models */,
|
||||
A26075A51EEC7125005DB03E /* pass.xcdatamodeld */,
|
||||
A2F4E20F1EED7F0A0011986E /* Helpers */,
|
||||
A260757A1EEC6F34005DB03E /* passKit.h */,
|
||||
A260757B1EEC6F34005DB03E /* Info.plist */,
|
||||
);
|
||||
path = passKit;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -376,11 +382,19 @@
|
|||
path = passExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A2C532B9201DD07500DB9F53 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A2C532BD201E5AA100DB9F53 /* PasscodeLockPresenter.swift */,
|
||||
A2C532BC201E5AA000DB9F53 /* PasscodeLockViewController.swift */,
|
||||
);
|
||||
name = Controllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A2F4E20E1EED7F040011986E /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A28C66631EF109D600A398A1 /* PasscodeLockConfiguration.swift */,
|
||||
A28C66641EF109D600A398A1 /* PasscodeLockRepository.swift */,
|
||||
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */,
|
||||
A2F4E2101EED800F0011986E /* GitCredential.swift */,
|
||||
A2F4E2111EED800F0011986E /* Password.swift */,
|
||||
A2F4E2121EED800F0011986E /* PasswordEntity.swift */,
|
||||
|
|
@ -398,6 +412,7 @@
|
|||
A2F4E21B1EED80160011986E /* NotificationNames.swift */,
|
||||
A2F4E21C1EED80160011986E /* UITextFieldExtension.swift */,
|
||||
A2F4E21D1EED80160011986E /* Utils.swift */,
|
||||
A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */,
|
||||
);
|
||||
name = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -429,30 +444,30 @@
|
|||
DC19400C1E4B39400077E0A3 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCD3C65D1EFB9BB400CBE842 /* SettingsSplitViewController.swift */,
|
||||
DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */,
|
||||
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorSettingTableViewController.swift */,
|
||||
DC037CB11E4CAB1700609409 /* AboutRepositoryTableViewController.swift */,
|
||||
DC962CDE1E4B62C10033B5D8 /* AboutTableViewController.swift */,
|
||||
DC037CB71E4DD1A500609409 /* AddPasswordTableViewController.swift */,
|
||||
DC193FF91E49B4430077E0A3 /* AdvancedSettingsTableViewController.swift */,
|
||||
DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */,
|
||||
DC3E64E51E656F11009A83DE /* CommitLogsTableViewController.swift */,
|
||||
DCFB77A61E502DF9008DE471 /* EditPasswordTableViewController.swift */,
|
||||
DC037CAF1E4CA51F00609409 /* GeneralSettingsTableViewController.swift */,
|
||||
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */,
|
||||
DCA049991E335CC800522E8F /* GitServerSettingTableViewController.swift */,
|
||||
DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */,
|
||||
DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */,
|
||||
A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */,
|
||||
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */,
|
||||
DCC441511E8F6C06008A90C4 /* RawPasswordViewController.swift */,
|
||||
DCFB77A81E502FF6008DE471 /* PasswordEditorTableViewController.swift */,
|
||||
DC5734AD1E439AD400D09270 /* PasswordsViewController.swift */,
|
||||
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorSettingTableViewController.swift */,
|
||||
DCA0499B1E3362F400522E8F /* PGPKeySettingTableViewController.swift */,
|
||||
A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */,
|
||||
DCC441511E8F6C06008A90C4 /* RawPasswordViewController.swift */,
|
||||
DCD3C65D1EFB9BB400CBE842 /* SettingsSplitViewController.swift */,
|
||||
DCAAF7441E2FA66800AB94BC /* SettingsTableViewController.swift */,
|
||||
DC037CA91E4B8EAE00609409 /* SpecialThanksTableViewController.swift */,
|
||||
DC8963BF1E38EEB900828B09 /* SSHKeySettingTableViewController.swift */,
|
||||
DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */,
|
||||
A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */,
|
||||
A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */,
|
||||
A217ACE31E9BBBBD00A1A6CF /* GitConfigSettingTableViewController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -477,21 +492,21 @@
|
|||
DC19400F1E4B3A9E0077E0A3 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A2802BF71E70813A00879216 /* SliderTableViewCell.swift */,
|
||||
A2802BF81E70813A00879216 /* SliderTableViewCell.xib */,
|
||||
DCFB77AA1E503729008DE471 /* ContentTableViewCell.swift */,
|
||||
DCFB779C1E4F40C7008DE471 /* FillPasswordTableViewCell.swift */,
|
||||
DCFB779D1E4F40C7008DE471 /* FillPasswordTableViewCell.xib */,
|
||||
DCFB77981E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift */,
|
||||
DCFB77991E4F3BCF008DE471 /* TitleTextFieldTableViewCell.xib */,
|
||||
DC4914941E434301007FF592 /* LabelTableViewCell.swift */,
|
||||
DCDDEAAF1E4639F300F68193 /* LabelTableViewCell.xib */,
|
||||
DC037CB91E4DD47B00609409 /* TextFieldTableViewCell.swift */,
|
||||
DC037CBA1E4DD47B00609409 /* TextFieldTableViewCell.xib */,
|
||||
DCDDEAB11E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift */,
|
||||
DCFB77A21E500D9C008DE471 /* PasswordDetailTitleTableViewCell.xib */,
|
||||
A2802BF71E70813A00879216 /* SliderTableViewCell.swift */,
|
||||
A2802BF81E70813A00879216 /* SliderTableViewCell.xib */,
|
||||
DC037CB91E4DD47B00609409 /* TextFieldTableViewCell.swift */,
|
||||
DC037CBA1E4DD47B00609409 /* TextFieldTableViewCell.xib */,
|
||||
DC037CBD1E4ED4E100609409 /* TextViewTableViewCell.swift */,
|
||||
DC037CBE1E4ED4E100609409 /* TextViewTableViewCell.xib */,
|
||||
DCFB77AA1E503729008DE471 /* ContentTableViewCell.swift */,
|
||||
DCFB77981E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift */,
|
||||
DCFB77991E4F3BCF008DE471 /* TitleTextFieldTableViewCell.xib */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -542,6 +557,7 @@
|
|||
DC917BED1E2F38C4000FDF54 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3012B06C2039D6E400BE1793 /* Yams.framework */,
|
||||
A2A61C101EEF8E3500CFE063 /* libPods-passKit.a */,
|
||||
A2A61C0C1EEF8DFE00CFE063 /* libPods-passExtension.a */,
|
||||
A2227D541EEE5E78002A69A9 /* libObjectivePGP.a */,
|
||||
|
|
@ -693,7 +709,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0900;
|
||||
LastUpgradeCheck = 0930;
|
||||
ORGANIZATIONNAME = "Bob Sun";
|
||||
TargetAttributes = {
|
||||
A26075771EEC6F34005DB03E = {
|
||||
|
|
@ -984,11 +1000,11 @@
|
|||
"$(SRCROOT)/Carthage/Build/iOS/SVProgressHUD.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/SwiftyUserDefaults.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/ObjectiveGit.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/PasscodeLock.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/FavIcon.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/KeychainAccess.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/OneTimePassword.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/Base32.framework",
|
||||
"$(SRCROOT)/Carthage/Build/iOS/Yams.framework",
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
|
|
@ -1019,14 +1035,16 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A28C66661EF109D600A398A1 /* PasscodeLockRepository.swift in Sources */,
|
||||
A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */,
|
||||
A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */,
|
||||
A2F4E2151EED800F0011986E /* Password.swift in Sources */,
|
||||
A28C66651EF109D600A398A1 /* PasscodeLockConfiguration.swift in Sources */,
|
||||
A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */,
|
||||
A2F4E21E1EED80160011986E /* AppError.swift in Sources */,
|
||||
A2F4E2171EED800F0011986E /* PasswordStore.swift in Sources */,
|
||||
A2F4E2211EED80160011986E /* NotificationNames.swift in Sources */,
|
||||
A2F4E2221EED80160011986E /* UITextFieldExtension.swift in Sources */,
|
||||
A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */,
|
||||
A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */,
|
||||
A2F4E2201EED80160011986E /* Globals.swift in Sources */,
|
||||
A2F4E2231EED80160011986E /* Utils.swift in Sources */,
|
||||
A2F4E21F1EED80160011986E /* DefaultsKeys.swift in Sources */,
|
||||
|
|
@ -1325,6 +1343,7 @@
|
|||
SKIP_INSTALL = YES;
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
|
@ -1358,6 +1377,7 @@
|
|||
SKIP_INSTALL = YES;
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
@ -1408,6 +1428,7 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
|
@ -1415,6 +1436,7 @@
|
|||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
|
@ -1441,6 +1463,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.2;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
|
@ -1465,6 +1488,7 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
|
@ -1472,6 +1496,7 @@
|
|||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
|
@ -1492,6 +1517,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.mssun.passforios;
|
||||
|
|
@ -1530,7 +1556,7 @@
|
|||
OTHER_LDFLAGS = "${inherited}";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "2e72f4af-b935-4970-9cd3-44d4cc24b646";
|
||||
PROVISIONING_PROFILE = "3c4f599a-ce77-4184-b4c4-edebf09cba3b";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match Development me.mssun.passforios";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "pass/Helpers/Objective-CBridgingHeader.h";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0910"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
@ -26,7 +26,6 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
|
|
@ -66,7 +65,6 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import PasscodeLock
|
||||
import SVProgressHUD
|
||||
import passKit
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
}
|
||||
|
||||
lazy var passcodeLockPresenter: PasscodeLockPresenter = {
|
||||
let presenter = PasscodeLockPresenter(mainWindow: self.window, configuration: PasscodeLockConfiguration.shared)
|
||||
let presenter = PasscodeLockPresenter(mainWindow: self.window)
|
||||
return presenter
|
||||
}()
|
||||
|
||||
|
|
@ -92,7 +91,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
passcodeLockPresenter = PasscodeLockPresenter(mainWindow: self.window, configuration: PasscodeLockConfiguration.shared)
|
||||
passcodeLockPresenter.present(windowLevel: UIApplication.shared.windows.last?.windowLevel)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,29 +195,12 @@
|
|||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="touchIDTableViewCell" textLabel="H2E-hP-Gyf" style="IBUITableViewCellStyleDefault" id="wB7-Km-Oel">
|
||||
<rect key="frame" x="0.0" y="267" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="wB7-Km-Oel" id="m90-X7-DbI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Touch ID" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="H2E-hP-Gyf">
|
||||
<rect key="frame" x="20" y="0.0" width="374" height="43.666666666666664"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="6U8-ue-MhL">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="advancedTableViewCell" textLabel="MKj-d0-8q3" style="IBUITableViewCellStyleDefault" id="tQN-gu-iRe">
|
||||
<rect key="frame" x="0.0" y="347" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="303" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tQN-gu-iRe" id="Xs0-LN-r43">
|
||||
<rect key="frame" x="0.0" y="0.0" width="376" height="43.666666666666664"/>
|
||||
|
|
@ -241,7 +224,7 @@
|
|||
<tableViewSection id="T7L-rR-R9W">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="aboutTableViewCell" textLabel="oqz-Hr-RAl" style="IBUITableViewCellStyleDefault" id="osS-xk-WRP">
|
||||
<rect key="frame" x="0.0" y="427" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="383" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="osS-xk-WRP" id="G6j-ij-rNr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="376" height="43.666666666666664"/>
|
||||
|
|
@ -273,7 +256,6 @@
|
|||
<outlet property="passcodeTableViewCell" destination="6Y0-mj-qhA" id="vkI-h5-GRo"/>
|
||||
<outlet property="passwordRepositoryTableViewCell" destination="2rc-ZW-XKd" id="aFq-K7-eIj"/>
|
||||
<outlet property="pgpKeyTableViewCell" destination="1ze-MS-Xbj" id="hXe-eD-0R4"/>
|
||||
<outlet property="touchIDTableViewCell" destination="wB7-Km-Oel" id="0fi-Sb-qMa"/>
|
||||
<segue destination="ZUt-x1-TJu" kind="showDetail" identifier="setPGPKeyByURLSegue" id="qRF-S1-bqF"/>
|
||||
<segue destination="ffY-rC-jhq" kind="showDetail" identifier="setPGPKeyByASCIISegue" id="mgi-Oe-i2X"/>
|
||||
</connections>
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ class OpenSourceComponentsTableViewController: BasicStaticTableViewController {
|
|||
["KeychainAccess",
|
||||
"https://github.com/kishikawakatsumi/KeychainAccess",
|
||||
"https://github.com/kishikawakatsumi/KeychainAccess/blob/master/LICENSE"],
|
||||
["PasscodeLock",
|
||||
"https://github.com/zahlz/SwiftPasscodeLock",
|
||||
"https://github.com/zahlz/SwiftPasscodeLock/blob/master/LICENSE.txt"],
|
||||
["ObjectiveGit",
|
||||
"https://github.com/libgit2/objective-git",
|
||||
"https://github.com/libgit2/objective-git/blob/master/LICENSE"],
|
||||
|
|
@ -35,6 +32,9 @@ class OpenSourceComponentsTableViewController: BasicStaticTableViewController {
|
|||
["SVProgressHUD",
|
||||
"https://github.com/SVProgressHUD/SVProgressHUD",
|
||||
"https://github.com/SVProgressHUD/SVProgressHUD/blob/master/LICENSE.txt"],
|
||||
["Yams",
|
||||
"https://github.com/jpsim/Yams",
|
||||
"https://github.com/jpsim/Yams/blob/master/LICENSE"],
|
||||
]
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
|
|||
private var navigationItemTitle: String?
|
||||
|
||||
private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()}
|
||||
private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""]
|
||||
private var sectionFooterTitles = ["", "", "Use YAML format for additional fields.", ""]
|
||||
private let nameSection = 0
|
||||
private let passwordSection = 1
|
||||
private let additionsSection = 2
|
||||
|
|
|
|||
|
|
@ -229,6 +229,11 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
// reset the data table if the disaply settings have been changed
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnReloadTableViewRelatedNotification), name: .passwordDisplaySettingChanged, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil)
|
||||
|
||||
// listen to the swipe back guesture
|
||||
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(self.respondToSwipeGesture))
|
||||
swipeRight.direction = UISwipeGestureRecognizerDirection.right
|
||||
self.view.addGestureRecognizer(swipeRight)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
@ -314,6 +319,15 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
|
|||
}
|
||||
}
|
||||
|
||||
@objc func respondToSwipeGesture(gesture: UIGestureRecognizer) {
|
||||
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
|
||||
// swipe right -> swipe back
|
||||
if swipeGesture.direction == .right && parentPasswordEntity != nil {
|
||||
self.backAction(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func backAction(_ sender: Any?) {
|
||||
guard SharedDefaults[.isShowFolderOn] else { return }
|
||||
var anim: CATransition? = transitionFromLeft
|
||||
|
|
|
|||
|
|
@ -9,25 +9,16 @@
|
|||
import UIKit
|
||||
import SVProgressHUD
|
||||
import CoreData
|
||||
import PasscodeLock
|
||||
import LocalAuthentication
|
||||
import passKit
|
||||
|
||||
class SettingsTableViewController: UITableViewController, UITabBarControllerDelegate {
|
||||
|
||||
lazy var touchIDSwitch: UISwitch = {
|
||||
let uiSwitch = UISwitch(frame: CGRect.zero)
|
||||
uiSwitch.onTintColor = Globals.blue
|
||||
uiSwitch.addTarget(self, action: #selector(touchIDSwitchAction), for: UIControlEvents.valueChanged)
|
||||
return uiSwitch
|
||||
}()
|
||||
|
||||
@IBOutlet weak var pgpKeyTableViewCell: UITableViewCell!
|
||||
@IBOutlet weak var touchIDTableViewCell: UITableViewCell!
|
||||
@IBOutlet weak var passcodeTableViewCell: UITableViewCell!
|
||||
@IBOutlet weak var passwordRepositoryTableViewCell: UITableViewCell!
|
||||
var setPasscodeLockAlert: UIAlertController?
|
||||
|
||||
let passwordStore = PasswordStore.shared
|
||||
var passcodeLockConfig = PasscodeLockConfiguration.shared
|
||||
var passcodeLock = PasscodeLock.shared
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
|
|
@ -77,8 +68,8 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
SVProgressHUD.show(withStatus: "Fetching PGP Key")
|
||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||
do {
|
||||
try self.passwordStore.initPGPKey(with: controller.armorPublicKeyTextView.text, keyType: .public)
|
||||
try self.passwordStore.initPGPKey(with: controller.armorPrivateKeyTextView.text, keyType: .secret)
|
||||
try self.passwordStore.initPGPKey(with: SharedDefaults[.pgpPublicKeyArmor] ?? "", keyType: .public)
|
||||
try self.passwordStore.initPGPKey(with: SharedDefaults[.pgpPrivateKeyArmor] ?? "", keyType: .secret)
|
||||
DispatchQueue.main.async {
|
||||
self.pgpKeyTableViewCell.detailTextLabel?.text = self.passwordStore.pgpKeyID
|
||||
SVProgressHUD.showSuccess(withStatus: "Success")
|
||||
|
|
@ -123,14 +114,6 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
// Security section, hide TouchID if the device doesn't support
|
||||
if section == 1 {
|
||||
if hasTouchID() {
|
||||
return 2
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
|
||||
|
|
@ -138,10 +121,9 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
super.viewDidLoad()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SettingsTableViewController.actOnPasswordStoreErasedNotification), name: .passwordStoreErased, object: nil)
|
||||
self.passwordRepositoryTableViewCell.detailTextLabel?.text = SharedDefaults[.gitURL]?.host
|
||||
touchIDTableViewCell.accessoryView = touchIDSwitch
|
||||
setPGPKeyTableViewCellDetailText()
|
||||
setPasswordRepositoryTableViewCellDetailText()
|
||||
setPasscodeLockTouchIDCells()
|
||||
setPasscodeLockCell()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
@ -149,38 +131,11 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
tabBarController!.delegate = self
|
||||
}
|
||||
|
||||
private func hasTouchID() -> Bool {
|
||||
let context = LAContext()
|
||||
var error: NSError?
|
||||
if context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) {
|
||||
return true
|
||||
} else {
|
||||
switch error!.code {
|
||||
case LAError.Code.touchIDNotEnrolled.rawValue:
|
||||
return true
|
||||
case LAError.Code.passcodeNotSet.rawValue:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isTouchIDEnabled() -> Bool {
|
||||
let context = LAContext()
|
||||
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||
}
|
||||
|
||||
private func setPasscodeLockTouchIDCells() {
|
||||
if passcodeLockConfig.repository.hasPasscode {
|
||||
private func setPasscodeLockCell() {
|
||||
if passcodeLock.hasPasscode {
|
||||
self.passcodeTableViewCell.detailTextLabel?.text = "On"
|
||||
passcodeLockConfig.isTouchIDAllowed = SharedDefaults[.isTouchIDOn]
|
||||
touchIDSwitch.isOn = SharedDefaults[.isTouchIDOn]
|
||||
} else {
|
||||
self.passcodeTableViewCell.detailTextLabel?.text = "Off"
|
||||
SharedDefaults[.isTouchIDOn] = false
|
||||
passcodeLockConfig.isTouchIDAllowed = SharedDefaults[.isTouchIDOn]
|
||||
touchIDSwitch.isOn = SharedDefaults[.isTouchIDOn]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,10 +158,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
@objc func actOnPasswordStoreErasedNotification() {
|
||||
setPGPKeyTableViewCellDetailText()
|
||||
setPasswordRepositoryTableViewCellDetailText()
|
||||
setPasscodeLockTouchIDCells()
|
||||
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: passcodeLockConfig)
|
||||
setPasscodeLockCell()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
|
@ -221,22 +173,7 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
}
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
@objc func touchIDSwitchAction(uiSwitch: UISwitch) {
|
||||
if !passcodeLockConfig.repository.hasPasscode || !isTouchIDEnabled() {
|
||||
// switch off
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
||||
uiSwitch.isOn = SharedDefaults[.isTouchIDOn] // SharedDefaults[.isTouchIDOn] should be false
|
||||
Utils.alert(title: "Notice", message: "Please enable Touch ID of your phone and setup the passcode lock for Pass.", controller: self, completion: nil)
|
||||
}
|
||||
} else {
|
||||
SharedDefaults[.isTouchIDOn] = uiSwitch.isOn
|
||||
passcodeLockConfig.isTouchIDAllowed = SharedDefaults[.isTouchIDOn]
|
||||
}
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: passcodeLockConfig)
|
||||
}
|
||||
|
||||
|
||||
func showPGPKeyActionSheet() {
|
||||
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
var urlActionTitle = "Download from URL"
|
||||
|
|
@ -314,21 +251,20 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
}
|
||||
|
||||
func showPasscodeActionSheet() {
|
||||
let passcodeChangeViewController = PasscodeLockViewController(state: .change, configuration: passcodeLockConfig)
|
||||
let passcodeRemoveViewController = PasscodeLockViewController(state: .remove, configuration: passcodeLockConfig)
|
||||
|
||||
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
let passcodeRemoveViewController = PasscodeLockViewController()
|
||||
|
||||
|
||||
let removePasscodeAction = UIAlertAction(title: "Remove Passcode", style: .destructive) { [weak self] _ in
|
||||
passcodeRemoveViewController.successCallback = { _ in
|
||||
self?.setPasscodeLockTouchIDCells()
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.passcodeLockPresenter = PasscodeLockPresenter(mainWindow: appDelegate.window, configuration: (self?.passcodeLockConfig)!)
|
||||
passcodeRemoveViewController.successCallback = {
|
||||
self?.passcodeLock.delete()
|
||||
self?.setPasscodeLockCell()
|
||||
}
|
||||
self?.present(passcodeRemoveViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
let changePasscodeAction = UIAlertAction(title: "Change Passcode", style: .default) { [weak self] _ in
|
||||
self?.present(passcodeChangeViewController, animated: true, completion: nil)
|
||||
self?.setPasscodeLock()
|
||||
}
|
||||
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||
|
|
@ -339,12 +275,50 @@ class SettingsTableViewController: UITableViewController, UITabBarControllerDele
|
|||
optionMenu.popoverPresentationController?.sourceRect = passcodeTableViewCell.bounds
|
||||
self.present(optionMenu, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func alertTextFieldDidChange(_ sender: UITextField) {
|
||||
// check whether we should enable the Save button in setPasscodeLockAlert
|
||||
if let setPasscodeLockAlert = self.setPasscodeLockAlert,
|
||||
let setPasscodeLockAlertTextFields0 = setPasscodeLockAlert.textFields?[0],
|
||||
let setPasscodeLockAlertTextFields1 = setPasscodeLockAlert.textFields?[1] {
|
||||
if sender == setPasscodeLockAlertTextFields0 || sender == setPasscodeLockAlertTextFields1 {
|
||||
// two passwords should be the same, and length >= 4
|
||||
let passcodeText = setPasscodeLockAlertTextFields0.text!
|
||||
let passcodeConfirmationText = setPasscodeLockAlertTextFields1.text!
|
||||
setPasscodeLockAlert.actions[0].isEnabled = passcodeText == passcodeConfirmationText && passcodeText.count >= 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setPasscodeLock() {
|
||||
let passcodeSetViewController = PasscodeLockViewController(state: .set, configuration: passcodeLockConfig)
|
||||
passcodeSetViewController.successCallback = { _ in
|
||||
self.setPasscodeLockTouchIDCells()
|
||||
// prepare the alert for setting the passcode
|
||||
setPasscodeLockAlert = UIAlertController(title: "Set passcode", message: "Fill in your passcode for Pass (at least 4 characters)", preferredStyle: .alert)
|
||||
setPasscodeLockAlert?.addTextField(configurationHandler: {(_ textField: UITextField) -> Void in
|
||||
textField.placeholder = "Password"
|
||||
textField.isSecureTextEntry = true
|
||||
textField.addTarget(self, action: #selector(self.alertTextFieldDidChange(_:)), for: UIControlEvents.editingChanged)
|
||||
})
|
||||
setPasscodeLockAlert?.addTextField(configurationHandler: {(_ textField: UITextField) -> Void in
|
||||
textField.placeholder = "Password Confirmation"
|
||||
textField.isSecureTextEntry = true
|
||||
textField.addTarget(self, action: #selector(self.alertTextFieldDidChange(_:)), for: UIControlEvents.editingChanged)
|
||||
})
|
||||
|
||||
// save action
|
||||
let saveAction = UIAlertAction(title: "Save", style: .default) { (action:UIAlertAction) -> Void in
|
||||
let passcode: String = self.setPasscodeLockAlert!.textFields![0].text!
|
||||
self.passcodeLock.save(passcode: passcode)
|
||||
// refresh the passcode lock cell ("On")
|
||||
self.setPasscodeLockCell()
|
||||
}
|
||||
present(passcodeSetViewController, animated: true, completion: nil)
|
||||
saveAction.isEnabled = false // disable the Save button by default
|
||||
|
||||
// cancel action
|
||||
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||
|
||||
// present
|
||||
setPasscodeLockAlert?.addAction(saveAction)
|
||||
setPasscodeLockAlert?.addAction(cancelAction)
|
||||
self.present(setPasscodeLockAlert!, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13173"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
|
@ -20,19 +20,19 @@
|
|||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="19l-R0-gP1">
|
||||
<rect key="frame" x="8" y="11" width="304" height="68"/>
|
||||
<rect key="frame" x="16" y="11" width="288" height="68"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="password" textAlignment="natural" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="k0U-2N-YaX" userLabel="Password">
|
||||
<rect key="frame" x="0.0" y="0.0" width="244" height="68"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="228" height="68"/>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="alphabet"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="alphabet" textContentType="password"/>
|
||||
<connections>
|
||||
<action selector="textFieldDidChange:" destination="KGk-i7-Jjw" eventType="editingChanged" id="U0t-2B-JxY"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hTh-ek-Xam" userLabel="Generate">
|
||||
<rect key="frame" x="244" y="0.0" width="30" height="68"/>
|
||||
<rect key="frame" x="228" y="0.0" width="30" height="68"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="l0l-7B-Tws"/>
|
||||
</constraints>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SZJ-aY-45Y" userLabel="Setting">
|
||||
<rect key="frame" x="274" y="0.0" width="30" height="68"/>
|
||||
<rect key="frame" x="258" y="0.0" width="30" height="68"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="D9D-FC-ANz"/>
|
||||
</constraints>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ class LabelTableViewCell: UITableViewCell {
|
|||
return
|
||||
}
|
||||
titleLabel.text = title
|
||||
switch title.lowercased() {
|
||||
case "password":
|
||||
if title.caseInsensitiveCompare("password") == .orderedSame {
|
||||
type = .password
|
||||
if isReveal {
|
||||
contentLabel.attributedText = Utils.attributedPassword(plainPassword: content)
|
||||
|
|
@ -52,7 +51,7 @@ class LabelTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
contentLabel.font = Globals.passwordFont
|
||||
case "hmac-based":
|
||||
} else if title.caseInsensitiveCompare("hmac-based") == .orderedSame {
|
||||
type = .HOTP
|
||||
if isReveal {
|
||||
contentLabel.text = content
|
||||
|
|
@ -60,11 +59,12 @@ class LabelTableViewCell: UITableViewCell {
|
|||
contentLabel.text = Globals.oneTimePasswordDots
|
||||
}
|
||||
contentLabel.font = Globals.passwordFont
|
||||
case "url":
|
||||
} else if title.lowercased().range(of: "url") != nil || verifyUrl(content) {
|
||||
type = .URL
|
||||
contentLabel.text = content
|
||||
contentLabel.font = UIFont.systemFont(ofSize: contentLabel.font.pointSize)
|
||||
default:
|
||||
} else {
|
||||
// default
|
||||
type = .other
|
||||
contentLabel.text = content
|
||||
contentLabel.font = UIFont.systemFont(ofSize: contentLabel.font.pointSize)
|
||||
|
|
@ -198,4 +198,12 @@ class LabelTableViewCell: UITableViewCell {
|
|||
}
|
||||
self.accessoryView = buttons
|
||||
}
|
||||
|
||||
private func verifyUrl(_ urlString: String?) -> Bool {
|
||||
guard let urlString = urlString,
|
||||
let _ = URL(string: urlString) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
|
@ -25,14 +25,17 @@
|
|||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<string>SUBQUERY (
|
||||
extensionItems,
|
||||
$extensionItem,
|
||||
SUBQUERY (
|
||||
$extensionItem.attachments,
|
||||
$attachment,
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.find-login-action" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
|
||||
).@count == $extensionItem.attachments.@count
|
||||
).@count == 1</string>
|
||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||
<string>passProcessor</string>
|
||||
</dict>
|
||||
|
|
|
|||
|
|
@ -7,27 +7,13 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PasscodeLock
|
||||
import passKit
|
||||
|
||||
// add a cancel button in the passcode lock view
|
||||
struct CancelableEnterPasscodeState: PasscodeLockStateType {
|
||||
let title: String = "Enter passcode"
|
||||
let description: String = "Enter passcode"
|
||||
let isCancellableAction = true
|
||||
var isTouchIDAllowed = true
|
||||
mutating func accept(passcode: String, from lock: PasscodeLockType) {
|
||||
if lock.repository.check(passcode: passcode) {
|
||||
lock.delegate?.passcodeLockDidSucceed(lock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cancel means cancel the extension
|
||||
class PasscodeLockViewControllerForExtension: PasscodeLockViewController {
|
||||
var originalExtensionContest: NSExtensionContext?
|
||||
public convenience init(extensionContext: NSExtensionContext?, state: PasscodeLockStateType, configuration: PasscodeLockConfigurationType, animateOnDismiss: Bool = true) {
|
||||
self.init(state: state, configuration: configuration, animateOnDismiss: animateOnDismiss)
|
||||
public convenience init(extensionContext: NSExtensionContext?) {
|
||||
self.init()
|
||||
originalExtensionContest = extensionContext
|
||||
}
|
||||
override func viewDidLoad() {
|
||||
|
|
@ -43,18 +29,20 @@ class PasscodeLockViewControllerForExtension: PasscodeLockViewController {
|
|||
class PasscodeExtensionDisplay {
|
||||
private var isPasscodePresented = false
|
||||
private let passcodeLockVC: PasscodeLockViewControllerForExtension
|
||||
private let extensionContext: NSExtensionContext?
|
||||
|
||||
init(extensionContext: NSExtensionContext?) {
|
||||
let cancelableEnter = CancelableEnterPasscodeState()
|
||||
passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext, state: cancelableEnter, configuration: PasscodeLockConfiguration.shared)
|
||||
self.extensionContext = extensionContext
|
||||
passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext)
|
||||
passcodeLockVC.dismissCompletionCallback = { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
passcodeLockVC.setCancellable(true)
|
||||
}
|
||||
|
||||
// present the passcode lock view if passcode is set and the view controller is not presented
|
||||
func presentPasscodeLockIfNeeded(_ extensionVC: ExtensionViewController) {
|
||||
guard PasscodeLockConfiguration.shared.repository.hasPasscode && !isPasscodePresented == true else {
|
||||
guard PasscodeLock.shared.hasPasscode && !isPasscodePresented == true else {
|
||||
return
|
||||
}
|
||||
isPasscodePresented = true
|
||||
|
|
|
|||
54
passKit/Controllers/PasscodeLockPresenter.swift
Normal file
54
passKit/Controllers/PasscodeLockPresenter.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// PasscodeLockPresenter.swift
|
||||
// PasscodeLock
|
||||
//
|
||||
// Created by Yishi Lin on 10/04/2018.
|
||||
// Copyright © 2018 Yishi Lin. All rights reserved.
|
||||
//
|
||||
// Inspired by SwiftPasscodeLock created by Yanko Dimitrov.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
open class PasscodeLockPresenter {
|
||||
|
||||
fileprivate var mainWindow: UIWindow?
|
||||
fileprivate var passcodeLockWindow: UIWindow?
|
||||
|
||||
public init(mainWindow window: UIWindow?) {
|
||||
self.mainWindow = window
|
||||
}
|
||||
|
||||
open func present(windowLevel: CGFloat?) {
|
||||
guard PasscodeLock.shared.hasPasscode else { return }
|
||||
|
||||
// dismiss the original window
|
||||
dismiss()
|
||||
|
||||
// new window
|
||||
mainWindow?.endEditing(true)
|
||||
passcodeLockWindow = UIWindow(frame: self.mainWindow!.frame)
|
||||
moveWindowsToFront(windowLevel: windowLevel)
|
||||
passcodeLockWindow?.isHidden = false
|
||||
|
||||
// new vc
|
||||
let passcodeLockVC = PasscodeLockViewController()
|
||||
let userDismissCompletionCallback = passcodeLockVC.dismissCompletionCallback
|
||||
passcodeLockVC.dismissCompletionCallback = { [weak self] in
|
||||
userDismissCompletionCallback?()
|
||||
self?.dismiss()
|
||||
}
|
||||
passcodeLockWindow?.rootViewController = passcodeLockVC
|
||||
}
|
||||
|
||||
open func dismiss() {
|
||||
passcodeLockWindow?.isHidden = true
|
||||
passcodeLockWindow?.rootViewController = nil
|
||||
}
|
||||
|
||||
fileprivate func moveWindowsToFront(windowLevel: CGFloat?) {
|
||||
let windowLevel = windowLevel ?? UIWindowLevelNormal
|
||||
let maxWinLevel = max(windowLevel, UIWindowLevelNormal)
|
||||
passcodeLockWindow?.windowLevel = maxWinLevel + 1
|
||||
}
|
||||
}
|
||||
212
passKit/Controllers/PasscodeLockViewController.swift
Normal file
212
passKit/Controllers/PasscodeLockViewController.swift
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
//
|
||||
// PasscodeLockPresenter.swift
|
||||
// PasscodeLock
|
||||
//
|
||||
// Created by Yishi Lin on 10/04/2018.
|
||||
// Copyright © 2018 Yishi Lin. All rights reserved.
|
||||
//
|
||||
// Inspired by SwiftPasscodeLock created by Yanko Dimitrov.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import LocalAuthentication
|
||||
|
||||
open class PasscodeLockViewController: UIViewController, UITextFieldDelegate {
|
||||
|
||||
open var dismissCompletionCallback: (()->Void)?
|
||||
open var successCallback: (()->Void)?
|
||||
open var cancelCallback: (()->Void)?
|
||||
|
||||
weak var passcodeLabel: UILabel?
|
||||
weak var passcodeWrongAttemptsLabel: UILabel?
|
||||
weak var passcodeTextField: UITextField?
|
||||
weak var biometryAuthButton: UIButton?
|
||||
open weak var cancelButton: UIButton?
|
||||
|
||||
var passcodeFailedAttempts = 0
|
||||
var isCancellable: Bool = false
|
||||
|
||||
open override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
let passcodeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 40))
|
||||
passcodeLabel.text = "Enter passcode for Pass"
|
||||
passcodeLabel.font = UIFont.boldSystemFont(ofSize: 18)
|
||||
passcodeLabel.textColor = UIColor.black
|
||||
passcodeLabel.textAlignment = .center
|
||||
passcodeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(passcodeLabel)
|
||||
self.passcodeLabel = passcodeLabel
|
||||
|
||||
let passcodeWrongAttemptsLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 300, height: 40))
|
||||
passcodeWrongAttemptsLabel.text = ""
|
||||
passcodeWrongAttemptsLabel.textColor = UIColor.red
|
||||
passcodeWrongAttemptsLabel.textAlignment = .center
|
||||
passcodeWrongAttemptsLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(passcodeWrongAttemptsLabel)
|
||||
self.passcodeWrongAttemptsLabel = passcodeWrongAttemptsLabel
|
||||
|
||||
let passcodeTextField = UITextField(frame: CGRect(x: 0, y: 0, width: 300, height: 40))
|
||||
passcodeTextField.borderStyle = UITextBorderStyle.roundedRect
|
||||
passcodeTextField.placeholder = "passcode"
|
||||
passcodeTextField.isSecureTextEntry = true
|
||||
passcodeTextField.clearButtonMode = UITextFieldViewMode.whileEditing
|
||||
passcodeTextField.delegate = self
|
||||
passcodeTextField.addTarget(self, action: #selector(self.passcodeTextFieldDidChange(_:)), for: UIControlEvents.editingChanged)
|
||||
self.view.backgroundColor = UIColor.white
|
||||
passcodeTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(passcodeTextField)
|
||||
self.passcodeTextField = passcodeTextField
|
||||
|
||||
let biometryAuthButton = UIButton(type: .custom)
|
||||
biometryAuthButton.setTitle("", for: .normal)
|
||||
biometryAuthButton.setTitleColor(Globals.blue, for: .normal)
|
||||
biometryAuthButton.addTarget(self, action: #selector(bioButtonPressedAction(_:)), for: .touchUpInside)
|
||||
biometryAuthButton.isHidden = true
|
||||
biometryAuthButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(biometryAuthButton)
|
||||
self.biometryAuthButton = biometryAuthButton
|
||||
|
||||
let myContext = LAContext()
|
||||
var authError: NSError?
|
||||
if #available(iOS 8.0, macOS 10.12.1, *) {
|
||||
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
|
||||
var biometryType = "Touch ID"
|
||||
if #available(iOS 11.0, *) {
|
||||
if myContext.biometryType == LABiometryType.faceID {
|
||||
biometryType = "Face ID"
|
||||
}
|
||||
}
|
||||
biometryAuthButton.setTitle(biometryType, for: .normal)
|
||||
biometryAuthButton.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
let cancelButton = UIButton(type: .custom)
|
||||
cancelButton.setTitle("Cancel", for: .normal)
|
||||
cancelButton.setTitleColor(Globals.blue, for: .normal)
|
||||
cancelButton.addTarget(self, action: #selector(passcodeLockDidCancel), for: .touchUpInside)
|
||||
cancelButton.isHidden = !self.isCancellable
|
||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
cancelButton.contentHorizontalAlignment = UIControlContentHorizontalAlignment.left
|
||||
self.view.addSubview(cancelButton)
|
||||
self.cancelButton = cancelButton
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
passcodeTextField.widthAnchor.constraint(equalToConstant: 300),
|
||||
passcodeTextField.heightAnchor.constraint(equalToConstant: 40),
|
||||
passcodeTextField.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
passcodeTextField.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -20),
|
||||
// above passocde
|
||||
passcodeLabel.widthAnchor.constraint(equalToConstant: 300),
|
||||
passcodeLabel.heightAnchor.constraint(equalToConstant: 40),
|
||||
passcodeLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
passcodeLabel.bottomAnchor.constraint(equalTo: passcodeTextField.topAnchor),
|
||||
// below passcode
|
||||
passcodeWrongAttemptsLabel.widthAnchor.constraint(equalToConstant: 300),
|
||||
passcodeWrongAttemptsLabel.heightAnchor.constraint(equalToConstant: 40),
|
||||
passcodeWrongAttemptsLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
passcodeWrongAttemptsLabel.topAnchor.constraint(equalTo: passcodeTextField.bottomAnchor),
|
||||
// bottom of the screen
|
||||
biometryAuthButton.widthAnchor.constraint(equalToConstant: 150),
|
||||
biometryAuthButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
biometryAuthButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
biometryAuthButton.bottomAnchor.constraint(equalTo: self.view.safeBottomAnchor, constant: -40),
|
||||
// cancel (top-left of the screen)
|
||||
cancelButton.widthAnchor.constraint(equalToConstant: 150),
|
||||
cancelButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
cancelButton.topAnchor.constraint(equalTo: self.view.safeTopAnchor),
|
||||
cancelButton.leftAnchor.constraint(equalTo: self.view.safeLeftAnchor, constant: 20)
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
open override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
open override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if let biometryAuthButton = biometryAuthButton {
|
||||
self.bioButtonPressedAction(biometryAuthButton)
|
||||
}
|
||||
}
|
||||
|
||||
internal func dismissPasscodeLock(completionHandler: (() -> Void)? = nil) {
|
||||
// clean up the textfield
|
||||
DispatchQueue.main.async {
|
||||
self.passcodeTextField?.text = ""
|
||||
}
|
||||
|
||||
// pop
|
||||
if presentingViewController?.presentedViewController == self {
|
||||
// if presented as modal
|
||||
dismiss(animated: true, completion: { [weak self] in
|
||||
self?.dismissCompletionCallback?()
|
||||
completionHandler?()
|
||||
})
|
||||
} else {
|
||||
// if pushed in a navigation controller
|
||||
_ = navigationController?.popViewController(animated: true)
|
||||
dismissCompletionCallback?()
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PasscodeLockDelegate
|
||||
|
||||
open func passcodeLockDidSucceed() {
|
||||
passcodeFailedAttempts = 0
|
||||
passcodeWrongAttemptsLabel?.text = ""
|
||||
dismissPasscodeLock(completionHandler: successCallback)
|
||||
}
|
||||
|
||||
@objc func passcodeLockDidCancel() {
|
||||
dismissPasscodeLock(completionHandler: cancelCallback)
|
||||
}
|
||||
|
||||
@objc func bioButtonPressedAction(_ uiButton: UIButton) {
|
||||
let myContext = LAContext()
|
||||
let myLocalizedReasonString = "Authentication is needed to access Pass."
|
||||
var authError: NSError?
|
||||
|
||||
if #available(iOS 8.0, *) {
|
||||
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
|
||||
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString) { success, evaluateError in
|
||||
if success {
|
||||
DispatchQueue.main.async {
|
||||
// user authenticated successfully, take appropriate action
|
||||
self.passcodeLockDidSucceed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if textField == passcodeTextField {
|
||||
if !PasscodeLock.shared.check(passcode: textField.text ?? "") {
|
||||
passcodeFailedAttempts = passcodeFailedAttempts + 1
|
||||
if passcodeFailedAttempts == 1 {
|
||||
passcodeWrongAttemptsLabel?.text = "1 wrong attempt"
|
||||
} else {
|
||||
passcodeWrongAttemptsLabel?.text = "\(passcodeFailedAttempts) wrong attempts"
|
||||
}
|
||||
}
|
||||
}
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func passcodeTextFieldDidChange(_ textField: UITextField) {
|
||||
if PasscodeLock.shared.check(passcode: textField.text ?? "") {
|
||||
self.passcodeLockDidSucceed()
|
||||
}
|
||||
}
|
||||
|
||||
public func setCancellable(_ isCancellable: Bool) {
|
||||
self.isCancellable = isCancellable
|
||||
cancelButton?.isHidden = !isCancellable
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ public enum AppError: Error {
|
|||
case GitResetError
|
||||
case PGPPublicKeyNotExistError
|
||||
case WrongPasswordFilename
|
||||
case YamlLoadError
|
||||
case UnknownError
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +37,8 @@ extension AppError: LocalizedError {
|
|||
return "PGP public key doesn't exist."
|
||||
case .WrongPasswordFilename:
|
||||
return "Cannot write to the password file."
|
||||
case .YamlLoadError:
|
||||
return "Cannot be parsed as a YAML file."
|
||||
case .UnknownError:
|
||||
return "Unknown error."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ extension UITextField {
|
|||
}
|
||||
|
||||
extension UIViewController {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if textField.nextField != nil {
|
||||
textField.nextField?.becomeFirstResponder()
|
||||
} else {
|
||||
|
|
|
|||
45
passKit/Helpers/UIViewExtension.swift
Normal file
45
passKit/Helpers/UIViewExtension.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// UIViewExtension.swift
|
||||
// passKit
|
||||
//
|
||||
// Created by Yishi Lin on 2018/4/11.
|
||||
// Copyright © 2018 Yishi Lin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UIView {
|
||||
|
||||
// Save anchors: https://stackoverflow.com/questions/46317061/use-safe-area-layout-programmatically
|
||||
var safeTopAnchor: NSLayoutYAxisAnchor {
|
||||
if #available(iOS 11.0, *) {
|
||||
return self.safeAreaLayoutGuide.topAnchor
|
||||
} else {
|
||||
return self.topAnchor
|
||||
}
|
||||
}
|
||||
|
||||
var safeLeftAnchor: NSLayoutXAxisAnchor {
|
||||
if #available(iOS 11.0, *){
|
||||
return self.safeAreaLayoutGuide.leftAnchor
|
||||
} else {
|
||||
return self.leftAnchor
|
||||
}
|
||||
}
|
||||
|
||||
var safeRightAnchor: NSLayoutXAxisAnchor {
|
||||
if #available(iOS 11.0, *){
|
||||
return self.safeAreaLayoutGuide.rightAnchor
|
||||
} else {
|
||||
return self.rightAnchor
|
||||
}
|
||||
}
|
||||
|
||||
var safeBottomAnchor: NSLayoutYAxisAnchor {
|
||||
if #available(iOS 11.0, *) {
|
||||
return self.safeAreaLayoutGuide.bottomAnchor
|
||||
} else {
|
||||
return self.bottomAnchor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>0.3.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
//
|
||||
// PasscodeRepository.swift
|
||||
// pass
|
||||
// PasscodeLock.swift
|
||||
// PassKit
|
||||
//
|
||||
// Created by Mingshen Sun on 7/2/2017.
|
||||
// Copyright © 2017 Bob Sun. All rights reserved.
|
||||
// Created by Yishi Lin on 28/1/2018.
|
||||
// Copyright © 2017 Yishi Lin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PasscodeLock
|
||||
import LocalAuthentication
|
||||
|
||||
public class PasscodeLockRepository: PasscodeRepositoryType {
|
||||
private let passcodeKey = "passcode.lock.passcode"
|
||||
open class PasscodeLock {
|
||||
public static let shared = PasscodeLock()
|
||||
|
||||
public var hasPasscode: Bool {
|
||||
|
||||
if passcode != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
fileprivate let passcodeKey = "passcode.lock.passcode"
|
||||
fileprivate var passcode: String? {
|
||||
return SharedDefaults[.passcodeKey]
|
||||
}
|
||||
|
||||
private var passcode: String? {
|
||||
return SharedDefaults[.passcodeKey]
|
||||
public var hasPasscode: Bool {
|
||||
return passcode != nil
|
||||
}
|
||||
|
||||
public func save(passcode: String) {
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// PasscodeLockConfiguration.swift
|
||||
// pass
|
||||
//
|
||||
// Created by Mingshen Sun on 7/2/2017.
|
||||
// Copyright © 2017 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PasscodeLock
|
||||
|
||||
public class PasscodeLockConfiguration: PasscodeLockConfigurationType {
|
||||
|
||||
public static let shared = PasscodeLockConfiguration()
|
||||
|
||||
public let repository: PasscodeRepositoryType
|
||||
public let passcodeLength = 4
|
||||
public var isTouchIDAllowed = SharedDefaults[.isTouchIDOn]
|
||||
|
||||
public let shouldRequestTouchIDImmediately = true
|
||||
public let maximumInccorectPasscodeAttempts = 3
|
||||
|
||||
init() {
|
||||
self.repository = PasscodeLockRepository()
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import Foundation
|
|||
import SwiftyUserDefaults
|
||||
import OneTimePassword
|
||||
import Base32
|
||||
import Yams
|
||||
|
||||
struct AdditionField {
|
||||
var title: String
|
||||
|
|
@ -24,6 +25,8 @@ enum PasswordChange: Int {
|
|||
|
||||
public class Password {
|
||||
public static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
|
||||
private static let OTPAUTH = "otpauth"
|
||||
private static let OTPAUTH_URL_START = "\(OTPAUTH)://"
|
||||
|
||||
public var name = ""
|
||||
public var url: URL?
|
||||
|
|
@ -84,29 +87,34 @@ public class Password {
|
|||
additions.removeAll()
|
||||
|
||||
// split the plain text
|
||||
let plainTextSplit = plainText.split(maxSplits: 1, omittingEmptySubsequences: false) {
|
||||
let plainTextSplit = self.plainText.split(omittingEmptySubsequences: false) {
|
||||
$0 == "\n" || $0 == "\r\n"
|
||||
}.map(String.init)
|
||||
|
||||
// get password
|
||||
password = plainTextSplit.first ?? ""
|
||||
|
||||
// get additonal fields
|
||||
if plainTextSplit.count == 2 {
|
||||
var unknownIndex = 0
|
||||
plainTextSplit[1].enumerateLines() { line, _ in
|
||||
if !line.isEmpty {
|
||||
var (key, value) = Password.getKeyValuePair(from: line)
|
||||
if key == nil {
|
||||
unknownIndex += 1
|
||||
key = "unknown \(unknownIndex)"
|
||||
}
|
||||
self.additions.append((key!, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
// get remaining lines (filter out empty lines)
|
||||
let additionalLines = plainTextSplit[1...].filter { !$0.isEmpty }
|
||||
|
||||
// check whether the first line of the plainText looks like an otp entry
|
||||
// separate normal lines (no otp tokens)
|
||||
let normalAdditionalLines = additionalLines.filter {
|
||||
!$0.hasPrefix(Password.OTPAUTH_URL_START)
|
||||
}.joined(separator: "\n")
|
||||
|
||||
// try to interpret the text format as YAML first
|
||||
do {
|
||||
try getAdditionalFields(fromYaml: normalAdditionalLines)
|
||||
}
|
||||
catch {
|
||||
getAdditionalFields(fromPlainText: normalAdditionalLines)
|
||||
}
|
||||
|
||||
// get and append otp tokens
|
||||
let otpAdditionalLines = additionalLines.filter { $0.hasPrefix(Password.OTPAUTH_URL_START) }
|
||||
otpAdditionalLines.forEach { self.additions.append((Password.OTPAUTH, $0)) }
|
||||
|
||||
// check whether the first line looks like an otp entry
|
||||
let (key, value) = Password.getKeyValuePair(from: self.password)
|
||||
if Password.otpKeywords.contains(key ?? "") {
|
||||
firstLineIsOTPField = true
|
||||
|
|
@ -119,6 +127,48 @@ public class Password {
|
|||
self.updateOtpToken()
|
||||
}
|
||||
|
||||
// check whether the file has lines with duplicated field names
|
||||
private func checkDuplicatedFields(lines: String) -> Bool {
|
||||
var keys = Set<String>()
|
||||
var hasDuplicatedFields = false
|
||||
lines.enumerateLines { (line, stop) -> () in
|
||||
let (key, _) = Password.getKeyValuePair(from: line)
|
||||
if let key = key {
|
||||
if keys.contains(key) {
|
||||
hasDuplicatedFields = true
|
||||
stop = true
|
||||
}
|
||||
keys.insert(key)
|
||||
}
|
||||
}
|
||||
return hasDuplicatedFields
|
||||
}
|
||||
|
||||
private func getAdditionalFields(fromYaml: String) throws {
|
||||
guard !fromYaml.isEmpty else { return }
|
||||
if checkDuplicatedFields(lines: fromYaml) {
|
||||
throw AppError.YamlLoadError
|
||||
}
|
||||
guard let yamlFile = try Yams.load(yaml: fromYaml) as? [String: Any] else {
|
||||
throw AppError.YamlLoadError
|
||||
}
|
||||
additions.append(contentsOf: yamlFile.map { ($0, String(describing: $1)) })
|
||||
}
|
||||
|
||||
private func getAdditionalFields(fromPlainText: String) {
|
||||
var unknownIndex = 0
|
||||
fromPlainText.enumerateLines() { line, _ in
|
||||
if !line.isEmpty {
|
||||
var (key, value) = Password.getKeyValuePair(from: line)
|
||||
if key == nil {
|
||||
unknownIndex += 1
|
||||
key = "unknown \(unknownIndex)"
|
||||
}
|
||||
self.additions.append((key!, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getFilteredAdditions() -> [(String, String)] {
|
||||
var filteredAdditions = [(String, String)]()
|
||||
additions.forEach { (key: String, value: String) in
|
||||
|
|
@ -153,8 +203,8 @@ public class Password {
|
|||
// no ": " found, or empty on both sides of ": "
|
||||
value = line
|
||||
// otpauth special case
|
||||
if value.hasPrefix("otpauth://") {
|
||||
key = "otpauth"
|
||||
if value.hasPrefix(Password.OTPAUTH_URL_START) {
|
||||
key = Password.OTPAUTH
|
||||
}
|
||||
} else {
|
||||
if !items[0].isEmpty {
|
||||
|
|
@ -221,9 +271,9 @@ public class Password {
|
|||
self.otpToken = nil
|
||||
|
||||
// get otpauth, if we are able to generate a token, return
|
||||
if var otpauthString = getAdditionValue(withKey: "otpauth") {
|
||||
if !otpauthString.hasPrefix("otpauth:") {
|
||||
otpauthString = "otpauth:\(otpauthString)"
|
||||
if var otpauthString = getAdditionValue(withKey: Password.OTPAUTH) {
|
||||
if !otpauthString.hasPrefix("\(Password.OTPAUTH):") {
|
||||
otpauthString = "\(Password.OTPAUTH):\(otpauthString)"
|
||||
}
|
||||
if let otpauthUrl = URL(string: otpauthString),
|
||||
let token = Token(url: otpauthUrl) {
|
||||
|
|
@ -347,7 +397,7 @@ public class Password {
|
|||
let (key, _) = Password.getKeyValuePair(from: line)
|
||||
if !Password.otpKeywords.contains(key ?? "") {
|
||||
lines.append(line)
|
||||
} else if key == "otpauth" && newOtpauth != nil {
|
||||
} else if key == Password.OTPAUTH && newOtpauth != nil {
|
||||
lines.append(newOtpauth!)
|
||||
// set to nil to prevent duplication
|
||||
newOtpauth = nil
|
||||
|
|
|
|||
|
|
@ -654,9 +654,9 @@ public class PasswordStore {
|
|||
|
||||
public func delete(passwordEntity: PasswordEntity) throws {
|
||||
let deletedFileURL = passwordEntity.getURL()!
|
||||
try deleteDirectoryTree(at: passwordEntity.getURL()!)
|
||||
try deletePasswordEntities(passwordEntity: passwordEntity)
|
||||
try gitRm(path: deletedFileURL.path)
|
||||
try deletePasswordEntities(passwordEntity: passwordEntity)
|
||||
try deleteDirectoryTree(at: deletedFileURL)
|
||||
let _ = try gitCommit(message: "Remove \(deletedFileURL.deletingPathExtension().path.removingPercentEncoding!) from store using Pass for iOS.")
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue